New Electron application. Import old AP.Mail HTML/JS version and improve it a bit.
This commit is contained in:
commit
b811b09fa5
|
@ -0,0 +1,193 @@
|
|||
/* Body */
|
||||
|
||||
body {
|
||||
background: hsl(240, 10%, 40%);
|
||||
margin-left: 1.5%;
|
||||
margin-right: 1.5%;
|
||||
|
||||
color: hsl(240, 10%, 15%);
|
||||
border-color: hsl(240, 10%, 15%);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: hsl(240, 10%, 75%);
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
border-radius: 0.5em;
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
text-align: center;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page:checked+label {
|
||||
background-color: hsl(240, 10%, 85%);
|
||||
}
|
||||
|
||||
.page-label {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
border-radius: 0.5em 0.5em 0 0;
|
||||
background-color: hsl(240, 10%, 65%);
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
/* Main */
|
||||
h1 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h5 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h6 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: hsl(240, 10%, 85%);
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
border-radius: 0 0.5em 0.5em 0.5em;
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: hsl(240, 10%, 75%);
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
border-radius: 0.5em;
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
text-align: center;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
/* Other */
|
||||
a:link {
|
||||
color: hsl(220, 100%, 40%);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:visited {
|
||||
color: hsl(280, 100%, 40%);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: hsl(220, 100%, 90%);
|
||||
text-shadow: 2px 0px 1px hsl(220, 100%, 30%), -2px 0px 1px hsl(220, 100%, 30%), 0px 2px 1px hsl(220, 100%, 30%), 0px -2px 1px hsl(220, 100%, 30%),
|
||||
1px 1px 0px hsl(220, 100%, 30%), 1px -1px 0px hsl(220, 100%, 30%), -1px 1px 0px hsl(220, 100%, 30%), -1px -1px 0px hsl(220, 100%, 30%);
|
||||
}
|
||||
|
||||
hr {
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
}
|
||||
|
||||
/* Page specific */
|
||||
ul.recipient-list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
li.actor-display {
|
||||
display: inline-block;
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
border-radius: 0.5em;
|
||||
padding: 0.1em;
|
||||
margin: 0;
|
||||
margin-left: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
background-color: hsl(240, 10%, 75%);
|
||||
}
|
||||
|
||||
article.activity:after {
|
||||
content: "";
|
||||
display: block;
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
margin: 0;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
section.actor-display {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0.1em;
|
||||
margin-left: 0.2em;
|
||||
margin-right: 0.2em;
|
||||
background-color: hsl(240, 10%, 75%);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
section.activity-object-field:after {
|
||||
content: "";
|
||||
display: block;
|
||||
border-style: dotted;
|
||||
border-width: thin;
|
||||
margin: 0;
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
section.activity-object-content {
|
||||
background-color: hsl(240, 10%, 95%);
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>AP.Mail</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
|
||||
<link href="apmail.css" rel="stylesheet" type="text/css" media="all" />
|
||||
<script src="render.js"></script>
|
||||
</head>
|
||||
<body onload="UI.checkConnection()">
|
||||
<nav id="tab-bar" style="visibility:hidden;">
|
||||
<!-- Profile page tab -->
|
||||
<input type="radio" id="profile-selector" class="page" name="page" value="profile" checked onchange="UI.showPage('profile')" />
|
||||
<label for="profile-selector" class="page-label" id="profile-selector-label">Profile</label>
|
||||
<!-- Send page tab -->
|
||||
<input type="radio" id="send-selector" class="page" name="page" value="send" checked onchange="UI.showPage('send')" />
|
||||
<label for="send-selector" class="page-label" id="send-selector-label">Send Message</label>
|
||||
<!-- Inbox tab -->
|
||||
<input type="radio" id="inbox-selector" class="page" name="page" value="inbox" onchange="UI.showPage('inbox')" />
|
||||
<label for="inbox-selector" class="page-label" id="inbox-selector-label">Inbox</label>
|
||||
<!-- Outbox tab -->
|
||||
<input type="radio" id="outbox-selector" class="page" name="page" value="outbox" onchange="UI.showPage('outbox')" />
|
||||
<label for="outbox-selector" class="page-label" id="outbox-selector-label">Outbox</label>
|
||||
<!-- Lookup user tab -->
|
||||
<input type="radio" id="lookup-selector" class="page" name="page" value="lookup" onchange="UI.showPage('lookup')" />
|
||||
<label for="lookup-selector" class="page-label" id="lookup-selector-label">Lookup User</label>
|
||||
<!-- Disconnection -->
|
||||
<input type="radio" id="disconnect-selector" class="page" name="page" value="disconnect" onchange="UI.showPage('select-user')" />
|
||||
<label for="disconnect-selector" class="page-label" id="disconnect-selector-label">Disconnection</label>
|
||||
</nav>
|
||||
<!-- Connection pages -->
|
||||
<main id="select-user">
|
||||
<h2>Connection</h2>
|
||||
Indicate the account to connect to. <br/>
|
||||
<input id="connect-username" type="text" placeholder="user@server" pattern=".*@.*" required /><br/>
|
||||
<button onclick="UI.selectUser()">Next</button>
|
||||
<section id="select-user-error"></section>
|
||||
</main>
|
||||
<main id="ask-password" style="display:none;">
|
||||
<h2>Password</h2>
|
||||
<section id="ask-password-user-info"></section>
|
||||
Enter the password for this account. <br/>
|
||||
<input id="connect-password" type="password" placeholder="password" /><br/>
|
||||
<button onclick="UI.showPage('select-user')">Back</button>
|
||||
<button onclick="UI.connectUser()">Connect</button>
|
||||
<section id="ask-password-error"></section>
|
||||
</main>
|
||||
<!-- Profile page -->
|
||||
<main id="profile" style="display:none;">
|
||||
<section id="profile-info"></section>
|
||||
</main>
|
||||
<!-- Send message page -->
|
||||
<main id="send" style="display:none;">
|
||||
<h4>Audiance</h4>
|
||||
<label for="send-message-public-visibility">General visibility</label>
|
||||
<select id="send-message-public-visibility" onchange="UI.updateSendVisibility()">
|
||||
<option value="to">Public (to), displayed in local/global timelines</option>
|
||||
<option value="cc">Public (cc), hidden from local/global timelines</option>
|
||||
<option value="non">Not public</option>
|
||||
</select><br/>
|
||||
<label for="send-message-follower-visibility">Follower's visibility</label>
|
||||
<select id="send-message-follower-visibility" onchange="UI.updateSendVisibility()">
|
||||
<option value="to">Public (to)</option>
|
||||
<option value="cc">Public (cc)</option>
|
||||
<option value="non">Not sent to followers</option>
|
||||
</select><br/>
|
||||
<label for="send-message-to-recipient">To</label>
|
||||
<ul id="send-message-to" class="recipient-list"></ul>
|
||||
<input id="send-message-to-recipient" type="text" placeholder="user@instance" size="40" />
|
||||
<button onclick="UI.addToRecipient()">Add</button><br/>
|
||||
<label for="send-message-cc-recipient">Cc</label>
|
||||
<ul id="send-message-cc" class="recipient-list"></ul>
|
||||
<input id="send-message-cc-recipient" type="text" placeholder="user@instance" size="40" />
|
||||
<button onclick="UI.addCcRecipient()">Add</button><br/>
|
||||
<section id="send-message-recipient-error"></section>
|
||||
<hr/>
|
||||
<h4>Message</h4>
|
||||
<input id="send-message-subject" type="text" onchange="UI.updateSendContent()" size="80" placeholder="Subject (optional)"/> <br/>
|
||||
<textarea id="send-message-content" rows="20" cols="80" placeholder="What do you want to say ?" onchange="UI.updateSendContent()"></textarea> <br/>
|
||||
<button onclick="UI.sendMessage()">Send</button>
|
||||
<section id="send-error"></section>
|
||||
</main>
|
||||
<!-- Inbox page -->
|
||||
<main id="inbox" style="display:none;">
|
||||
<section id="inbox-messages"></section>
|
||||
<section id="inbox-messages-error"></section>
|
||||
</main>
|
||||
<!-- Outbox page -->
|
||||
<main id="outbox" style="display:none;">
|
||||
<section id="outbox-messages"></section>
|
||||
<section id="outbox-messages-error"></section>
|
||||
</main>
|
||||
<!-- Lookup page -->
|
||||
<main id="lookup" style="display:none;">
|
||||
<input id="lookup-user" type="text" placeholder="user@instance"/> <button onclick="UI.lookupUser()">Lookup user</button> <br/><hr/>
|
||||
<section id="lookup-user-error"></section>
|
||||
<section id="lookup-user-info"></section><hr/>
|
||||
<section id="lookup-user-timeline"></section>
|
||||
<section id="lookup-user-timeline-error"></section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,47 @@
|
|||
const { app, BrowserWindow } = require('electron')
|
||||
|
||||
function createWindow () {
|
||||
// Create the browser window.
|
||||
const win = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
}
|
||||
})
|
||||
|
||||
// and load the index.html of the app.
|
||||
win.loadFile('index.html')
|
||||
|
||||
// Open the DevTools.
|
||||
win.webContents.openDevTools()
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(createWindow)
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('web-contents-created', (event, contents) => {
|
||||
contents.on('will-navigate', (event, navigationUrl) => {
|
||||
// Disable all navigation to external webpages
|
||||
event.preventDefault()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "apmail",
|
||||
"version": "1.0.0",
|
||||
"description": "An ActivityPub Client",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron ."
|
||||
},
|
||||
"author": "Feufochmar",
|
||||
"license": "ISC"
|
||||
}
|
|
@ -0,0 +1,361 @@
|
|||
const {Actor} = require('./src/actor.js')
|
||||
const {Timeline} = require('./src/timeline.js')
|
||||
const {Message} = require('./src/message.js')
|
||||
const {ConnectedUser} = require('./src/connected-user.js')
|
||||
|
||||
// UI actions
|
||||
var UI = {
|
||||
// Attributes
|
||||
composed_message: new Message(),
|
||||
// Methods
|
||||
// Get the value of an element
|
||||
getValue: function(id) {
|
||||
return window.document.getElementById(id).value
|
||||
},
|
||||
// Set the value of an element
|
||||
setValue: function(id, val) {
|
||||
window.document.getElementById(id).value = val
|
||||
},
|
||||
// Clear the value of an element
|
||||
clearValue: function(id) {
|
||||
UI.setValue(id, '')
|
||||
},
|
||||
// Actor display
|
||||
renderActorTag: function(actor) {
|
||||
var display = '<section class="actor-display">'
|
||||
if (actor.valid) {
|
||||
if (actor.info.icon) {
|
||||
display = display + '<img src="' + actor.info.icon + '" width="32" height="32" /> '
|
||||
}
|
||||
display = display
|
||||
+ '<p style="display:inline-block;"><strong>' + actor.info.display_name + '</strong> <br/>'
|
||||
+ '<a href="' + actor.urls.profile + '">'
|
||||
+ actor.name + '@' + actor.server
|
||||
+ '</a></p>'
|
||||
} else {
|
||||
display = display
|
||||
+ '<p style="display:inline-block;">'
|
||||
+ '<a href="' + actor.urls.profile + '">'
|
||||
+ 'Other actor'
|
||||
+ '</a></p>'
|
||||
}
|
||||
display = display + '</section>'
|
||||
return display
|
||||
},
|
||||
// Show the page tab
|
||||
showPageTab: function(is_shown) {
|
||||
window.document.getElementById('tab-bar').style.visibility = is_shown ? 'visible' : 'hidden'
|
||||
},
|
||||
// Action done when refreshing a page
|
||||
refreshPage: {
|
||||
'select-user': function() {
|
||||
UI.showPageTab(false)
|
||||
ConnectedUser.disconnect()
|
||||
UI.clearValue('connect-username')
|
||||
},
|
||||
'ask-password': function() {
|
||||
UI.showPageTab(false)
|
||||
UI.clearValue('connect-password')
|
||||
window.document.getElementById('ask-password-user-info').innerHTML = UI.renderActorTag(ConnectedUser.actor)
|
||||
},
|
||||
'profile': function() {
|
||||
UI.showPageTab(true)
|
||||
UI.updateProfilePage()
|
||||
},
|
||||
'send': function() {
|
||||
UI.showPageTab(true)
|
||||
UI.composed_message = new Message()
|
||||
UI.updateSendMessagePage()
|
||||
},
|
||||
'inbox': function() {
|
||||
UI.showPageTab(true)
|
||||
window.document.getElementById('inbox-messages-error').innerHTML = 'Loading inbox...'
|
||||
UI.showTimeline('inbox-messages', ConnectedUser.tokens.user.access_token, ConnectedUser.actor.urls.inbox)
|
||||
},
|
||||
'outbox': function() {
|
||||
UI.showPageTab(true)
|
||||
window.document.getElementById('outbox-messages-error').innerHTML = 'Loading outbox...'
|
||||
UI.showTimeline('outbox-messages', ConnectedUser.tokens.user.access_token, ConnectedUser.actor.urls.outbox)
|
||||
},
|
||||
'lookup': function() {
|
||||
UI.showPageTab(true)
|
||||
window.document.getElementById('lookup-user-error').innerHTML = ''
|
||||
window.document.getElementById('lookup-user-info').innerHTML = ''
|
||||
window.document.getElementById('lookup-user-timeline').innerHTML = ''
|
||||
window.document.getElementById('lookup-user-timeline-error').innerHTML = ''
|
||||
}
|
||||
},
|
||||
// Show a given page
|
||||
showPage: function(page) {
|
||||
['select-user', 'ask-password', 'profile', 'send', 'inbox', 'outbox', 'lookup'].map(x => window.document.getElementById(x).style.display = 'none')
|
||||
window.document.getElementById(page).style.display = 'block'
|
||||
UI.refreshPage[page]()
|
||||
},
|
||||
// When the page loads, load the connected user if already connected
|
||||
checkConnection: function() {
|
||||
ConnectedUser.loadFromLocalStorage(
|
||||
function(load_ok, failure_message) {
|
||||
if (load_ok) {
|
||||
// TODO: Refresh the access token ?
|
||||
// No need to show the login pages, go directly to send message page
|
||||
UI.showPage('send')
|
||||
} else {
|
||||
// Show the select user page
|
||||
UI.showPage('select-user')
|
||||
}
|
||||
})
|
||||
},
|
||||
// Get the username + servername from an address "user@server"
|
||||
getUserAndServer: function(address) {
|
||||
var names = address.split('@')
|
||||
return {
|
||||
user: names[0],
|
||||
server: names[1]
|
||||
}
|
||||
},
|
||||
// When the user enter its address, load the actor representing the user
|
||||
selectUser: function() {
|
||||
// Get the user name and server name
|
||||
var names = UI.getUserAndServer(window.document.getElementById('connect-username').value)
|
||||
// Load the actor and go to the ask-password page
|
||||
ConnectedUser.actor.loadFromNameAndServer(
|
||||
names.user, names.server,
|
||||
function(load_ok, failure_message) {
|
||||
if (load_ok) {
|
||||
UI.showPage('ask-password')
|
||||
} else {
|
||||
window.document.getElementById('select-user-error').innerText = 'Error: ' + failure_message
|
||||
}
|
||||
})
|
||||
},
|
||||
// When the user enter its password, get the access tokens and then go to the send page
|
||||
connectUser: function() {
|
||||
// Connect the user and go to the send page
|
||||
ConnectedUser.connect(
|
||||
UI.getValue('connect-password'),
|
||||
function(load_ok, failure_message) {
|
||||
if (load_ok) {
|
||||
UI.showPage('send')
|
||||
} else {
|
||||
window.document.getElementById('ask-password-error').innerText = 'Error: ' + failure_message
|
||||
}
|
||||
})
|
||||
},
|
||||
updateProfilePage: function() {
|
||||
window.document.getElementById('profile-info').innerHTML = UI.renderActor(ConnectedUser.actor)
|
||||
},
|
||||
updateSendMessagePage: function() {
|
||||
UI.clearValue('send-message-to-recipient')
|
||||
UI.clearValue('send-message-cc-recipient')
|
||||
UI.setValue('send-message-public-visibility', UI.composed_message.public_visibility)
|
||||
UI.setValue('send-message-follower-visibility', UI.composed_message.follower_visibility)
|
||||
UI.setValue('send-message-subject', UI.composed_message.subject)
|
||||
UI.setValue('send-message-content', UI.composed_message.content)
|
||||
// TO/CC
|
||||
window.document.getElementById('send-message-to').innerHTML = UI.composed_message.to.map(
|
||||
function(element) {
|
||||
return '<li class="actor-display">' + UI.renderActorTag(element) + ' <button onclick="UI.removeToRecipient(\'' + element.urls.profile + '\')">×</button></li>'
|
||||
}).join('')
|
||||
window.document.getElementById('send-message-cc').innerHTML = UI.composed_message.cc.map(
|
||||
function(element) {
|
||||
return '<li class="actor-display">' + UI.renderActorTag(element) + ' <button onclick="UI.removeCcRecipient(\'' + element.urls.profile + '\')">×</button></li>'
|
||||
}).join('')
|
||||
// Errors
|
||||
window.document.getElementById('send-message-recipient-error').innerHTML = ''
|
||||
window.document.getElementById('send-error').innerHTML = ''
|
||||
},
|
||||
updateSendVisibility: function() {
|
||||
UI.composed_message.setVisibility(UI.getValue('send-message-public-visibility'), UI.getValue('send-message-follower-visibility'))
|
||||
},
|
||||
updateSendContent: function() {
|
||||
UI.composed_message.setContent(UI.getValue('send-message-subject'), UI.getValue('send-message-content'))
|
||||
},
|
||||
// Add to recipient lists
|
||||
addToRecipient: function() {
|
||||
var names = UI.getUserAndServer(window.document.getElementById('send-message-to-recipient').value)
|
||||
var actor = new Actor()
|
||||
actor.loadFromNameAndServer(
|
||||
names.user, names.server,
|
||||
function(load_ok, failure_message) {
|
||||
if (load_ok) {
|
||||
UI.composed_message.addToRecipient(actor)
|
||||
UI.updateSendMessagePage()
|
||||
} else {
|
||||
window.document.getElementById('send-message-recipient-error').innerHTML = 'Unable to find user (' + failure_message + ')'
|
||||
}
|
||||
})
|
||||
},
|
||||
addCcRecipient: function() {
|
||||
var names = UI.getUserAndServer(window.document.getElementById('send-message-cc-recipient').value)
|
||||
var actor = new Actor()
|
||||
actor.loadFromNameAndServer(
|
||||
names.user, names.server,
|
||||
function(load_ok, failure_message) {
|
||||
if (load_ok) {
|
||||
UI.composed_message.addCcRecipient(actor)
|
||||
UI.updateSendMessagePage()
|
||||
} else {
|
||||
window.document.getElementById('send-message-recipient-error').innerHTML = 'Unable to find user (' + failure_message + ')'
|
||||
}
|
||||
})
|
||||
},
|
||||
// Remove from recipient lists
|
||||
removeToRecipient: function(url_profile) {
|
||||
// Don't fetch the actor, only set the profile url used in removal
|
||||
var actor = new Actor()
|
||||
actor.urls.profile = url_profile
|
||||
//
|
||||
UI.composed_message.removeToRecipient(actor)
|
||||
UI.updateSendMessagePage()
|
||||
},
|
||||
removeCcRecipient: function(url_profile) {
|
||||
var actor = new Actor()
|
||||
actor.urls.profile = url_profile
|
||||
//
|
||||
UI.composed_message.removeCcRecipient(actor)
|
||||
UI.updateSendMessagePage()
|
||||
},
|
||||
// Send message
|
||||
sendMessage: function() {
|
||||
UI.composed_message.send(
|
||||
function(is_ok, failure_message) {
|
||||
if (is_ok) {
|
||||
UI.showPage('send')
|
||||
} else {
|
||||
window.document.getElementById('send-error').innerText = failure_message
|
||||
}
|
||||
})
|
||||
},
|
||||
renderRawActivity: function(activity) {
|
||||
var str_activity = JSON.stringify(activity.raw, null, 1)
|
||||
var replace_map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
'\'': '''
|
||||
}
|
||||
str_activity = str_activity.replace(/[&<>"']/g, x => replace_map[x])
|
||||
return '<section class="activity-raw"><details>'
|
||||
+ '<summary>Raw activity</summary>'
|
||||
+ '<textarea rows="30" cols="120" readonly>' + str_activity + '</textarea>'
|
||||
+ '</details></section>'
|
||||
},
|
||||
renderObject: {
|
||||
'Note': function(activity) {
|
||||
return '<article class="activity create note">'
|
||||
+ '<section class="activity-object-field">From ' + (activity.actor ? UI.renderActorTag(activity.actor) : '')
|
||||
+ ' – ' + (activity.published ? activity.published.toLocaleString() : '') + '</section>'
|
||||
+ '<section class="activity-object-field"> To ' + (activity.to ? activity.to.map(x => UI.renderActorTag(x)).join(' ') : '' ) + '</section>'
|
||||
+ '<section class="activity-object-field"> Cc ' + (activity.cc ? activity.cc.map(x => UI.renderActorTag(x)).join(' ') : '' ) + '</section>'
|
||||
+ '<section class="activity-object-field">Subject: ' + (activity.object.summary ? activity.object.summary : '' ) + '</section>'
|
||||
+ '<section class="activity-object-field activity-object-content">' + (activity.object.content ? activity.object.content : '' ) + '</section>'
|
||||
+ UI.renderRawActivity(activity)
|
||||
+ '</article>'
|
||||
}
|
||||
},
|
||||
renderActivity: {
|
||||
'Create': function(activity) {
|
||||
if (UI.renderObject[activity.object.type]) {
|
||||
return UI.renderObject[activity.object.type](activity)
|
||||
} else {
|
||||
return '<article class="activity create">'
|
||||
+ 'Object creation (' + activity.object.type + ').'
|
||||
+ UI.renderRawActivity(activity)
|
||||
+ '</article>'
|
||||
}
|
||||
},
|
||||
'Like': function(activity) {
|
||||
return '<article class="activity like">'
|
||||
+ (activity.actor ? UI.renderActorTag(activity.actor) : '')
|
||||
+ " liked "
|
||||
+ (activity.object ? '<a href="' + activity.object + '">' + activity.object + '</a>' : '')
|
||||
+ UI.renderRawActivity(activity)
|
||||
+ '</article>'
|
||||
},
|
||||
'Announce': function(activity) {
|
||||
return '<article class="activity share">'
|
||||
+ (activity.actor ? UI.renderActorTag(activity.actor) : '')
|
||||
+ " shared "
|
||||
+ (activity.object ? '<a href="' + activity.object + '">' + activity.object + '</a>' : '')
|
||||
+ UI.renderRawActivity(activity)
|
||||
+ '</article>'
|
||||
},
|
||||
'Delete': function(activity) {
|
||||
return '<article class="activity delete">'
|
||||
+ (activity.actor ? UI.renderActorTag(activity.actor) : '')
|
||||
+ " deleted an object."
|
||||
+ UI.renderRawActivity(activity)
|
||||
+ '</article>'
|
||||
}
|
||||
},
|
||||
showTimeline: function(id, token, url) {
|
||||
var timeline = new Timeline()
|
||||
timeline.load(url, token, function(load_ok, failure_message) {
|
||||
if (load_ok) {
|
||||
var content = timeline.activities.map(
|
||||
function(activity) {
|
||||
if (UI.renderActivity[activity.type]) {
|
||||
return UI.renderActivity[activity.type](activity)
|
||||
} else {
|
||||
return '<article class="activity">'
|
||||
+ 'Other activity (' + activity.type + ').'
|
||||
+ UI.renderRawActivity(activity)
|
||||
+ '</article>'
|
||||
}
|
||||
}).join('')
|
||||
content = content + '<section class="prev-next">'
|
||||
if (timeline.prev) {
|
||||
content = content + '<button onclick="UI.showTimeline(\'' + id + '\',' + (token ? '\'' + token + '\'' : 'undefined') + ',\'' + timeline.prev + '\')">Prev</button>'
|
||||
}
|
||||
if (timeline.next) {
|
||||
content = content + '<button onclick="UI.showTimeline(\'' + id + '\',' + (token ? '\'' + token + '\'' : 'undefined') + ',\'' + timeline.next + '\')">Next</button>'
|
||||
}
|
||||
content = content + '</section>'
|
||||
window.document.getElementById(id).innerHTML = content
|
||||
} else {
|
||||
window.document.getElementById(id + '-error').innerText = failure_message
|
||||
}
|
||||
})
|
||||
},
|
||||
renderActor: function(actor) {
|
||||
var display = '<section>'
|
||||
if (actor.valid) {
|
||||
if (actor.info.icon) {
|
||||
display = display + '<img src="' + actor.info.icon + '" width="96" height="96" /> '
|
||||
}
|
||||
display = display
|
||||
+ '<p style="display:inline-block;"><strong>' + actor.info.display_name + '</strong> <br/>'
|
||||
+ '<a href="' + actor.urls.profile + '">'
|
||||
+ actor.name + '@' + actor.server
|
||||
+ '</a></p>'
|
||||
+ '<p>' + actor.info.summary + '</p>'
|
||||
} else {
|
||||
display = display
|
||||
+ '<p style="display:inline-block;">'
|
||||
+ '<a href="' + actor.urls.profile + '">'
|
||||
+ 'Other actor'
|
||||
+ '</a></p>'
|
||||
}
|
||||
display = display + '</section>'
|
||||
return display
|
||||
},
|
||||
lookupUser: function() {
|
||||
window.document.getElementById('lookup-user-info').innerHTML = ''
|
||||
window.document.getElementById('lookup-user-timeline').innerHTML = ''
|
||||
window.document.getElementById('lookup-user-error').innerHTML = ''
|
||||
window.document.getElementById('lookup-user-timeline-error').innerHTML = ''
|
||||
var names = UI.getUserAndServer(window.document.getElementById('lookup-user').value)
|
||||
var actor = new Actor()
|
||||
actor.loadFromNameAndServer(
|
||||
names.user, names.server,
|
||||
function(load_ok, failure_message) {
|
||||
if (load_ok) {
|
||||
window.document.getElementById('lookup-user-info').innerHTML = UI.renderActor(actor)
|
||||
UI.showTimeline('lookup-user-timeline', undefined, actor.urls.outbox)
|
||||
} else {
|
||||
window.document.getElementById('lookup-user-error').innerHTML = 'Unable to find user (' + failure_message + ')'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
const {Actor} = require('./actor.js')
|
||||
const {KnownActors} = require('./known-actors.js')
|
||||
|
||||
// Activity class
|
||||
var Activity = function(raw_activity) {
|
||||
this.raw = raw_activity
|
||||
this.type = raw_activity.type
|
||||
this.published = raw_activity.published ? new Date(raw_activity.published) : undefined
|
||||
this.object = raw_activity.object
|
||||
this.public_visibility = 'non'
|
||||
// actor, to, cc are filled in loadActors
|
||||
this.actor = undefined
|
||||
this.to = []
|
||||
this.cc = []
|
||||
}
|
||||
Activity.prototype = {
|
||||
// Load the actors present in the actor, to and cc fields
|
||||
loadActors: function(callback) {
|
||||
// Load the actor
|
||||
var profile = this.raw.actor
|
||||
KnownActors.retrieve(
|
||||
profile,
|
||||
function(load_ok, failure_message) {
|
||||
if (load_ok) {
|
||||
this.actor = KnownActors.get(profile)
|
||||
}
|
||||
// Load the actors in the "to" array, skip to "cc" if there is no "to", and stop is there is no "cc" either
|
||||
if (this.raw.to) {
|
||||
this.loadToActors(this.raw.to.values(), callback)
|
||||
} else if (this.raw.cc) {
|
||||
this.loadCcActors(this.raw.cc.values(), callback)
|
||||
} else {
|
||||
callback(true, 'ok')
|
||||
}
|
||||
}.bind(this))
|
||||
},
|
||||
// Load the "To" array
|
||||
loadToActors: function(iter, callback) {
|
||||
var next = iter.next()
|
||||
if (next.done) {
|
||||
// Finished: load the "cc" array (if possible)
|
||||
if (this.raw.cc) {
|
||||
this.loadCcActors(this.raw.cc.values(), callback)
|
||||
} else {
|
||||
callback(true, 'ok')
|
||||
}
|
||||
} else {
|
||||
var profile = next.value
|
||||
if (profile === 'https://www.w3.org/ns/activitystreams#Public') {
|
||||
this.public_visibility = 'to'
|
||||
this.loadToActors(iter, callback)
|
||||
} else {
|
||||
KnownActors.retrieve(
|
||||
profile,
|
||||
function(load_ok, failure_message) {
|
||||
if (load_ok) {
|
||||
this.to.push(KnownActors.get(profile))
|
||||
} else {
|
||||
// Collection ?
|
||||
}
|
||||
this.loadToActors(iter, callback)
|
||||
}.bind(this))
|
||||
}
|
||||
}
|
||||
},
|
||||
// Load the "Cc" array
|
||||
loadCcActors: function(iter, callback) {
|
||||
var next = iter.next()
|
||||
if (next.done) {
|
||||
callback(true, 'ok')
|
||||
} else {
|
||||
var profile = next.value
|
||||
if (profile === 'https://www.w3.org/ns/activitystreams#Public') {
|
||||
this.public_visibility = 'cc'
|
||||
this.loadCcActors(iter, callback)
|
||||
} else {
|
||||
KnownActors.retrieve(
|
||||
profile,
|
||||
function(load_ok, failure_message) {
|
||||
if (load_ok) {
|
||||
this.cc.push(KnownActors.get(profile))
|
||||
} else {
|
||||
// Collection ?
|
||||
}
|
||||
this.loadCcActors(iter, callback)
|
||||
}.bind(this))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exported structures
|
||||
exports.Activity = Activity
|
|
@ -0,0 +1,123 @@
|
|||
// Actor class
|
||||
// ActorInfo structure
|
||||
var ActorInfo = function() {}
|
||||
ActorInfo.prototype = {
|
||||
display_name: undefined,
|
||||
summary: undefined,
|
||||
icon: undefined,
|
||||
type: undefined
|
||||
}
|
||||
// ActorUrls structure
|
||||
var ActorUrls = function() {}
|
||||
ActorUrls.prototype = {
|
||||
profile: undefined,
|
||||
outbox: undefined,
|
||||
inbox: undefined,
|
||||
followers: undefined,
|
||||
following: undefined,
|
||||
oauth_token: undefined,
|
||||
oauth_registration: undefined,
|
||||
oauth_authorization: undefined
|
||||
}
|
||||
// Actor structure
|
||||
var Actor = function() {
|
||||
this.info = new ActorInfo()
|
||||
this.urls = new ActorUrls()
|
||||
}
|
||||
Actor.prototype = {
|
||||
// Attributes
|
||||
name: undefined,
|
||||
server: undefined,
|
||||
info: undefined,
|
||||
urls: undefined,
|
||||
valid: false,
|
||||
// Methods
|
||||
// Load from name@server.
|
||||
// Callback is a function accepting two arguments:
|
||||
// - a boolean indicating if the loading is complete or in failure,
|
||||
// - a string indicating the failure
|
||||
loadFromNameAndServer: function(name, server, callback) {
|
||||
this.name = name
|
||||
this.server = server
|
||||
// Use Webfinger to find the profile URL
|
||||
var request = new XMLHttpRequest()
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState == 4 && request.status == 200) {
|
||||
var answer = JSON.parse(request.responseText)
|
||||
if (answer && answer.links) {
|
||||
var link = answer.links.find(e => e.type === 'application/activity+json');
|
||||
var profile = undefined
|
||||
if (link) {
|
||||
profile = link.href
|
||||
}
|
||||
if (profile) {
|
||||
this.loadFromProfileUrl(profile, callback)
|
||||
} else {
|
||||
callback(false, 'webfinger: account exists on server, but no ActivityPub account found.')
|
||||
}
|
||||
} else {
|
||||
callback(false, 'webfinger: incorrect response from server.')
|
||||
console.log(answer)
|
||||
}
|
||||
} else if (request.readyState == 4) {
|
||||
callback(false, 'webfinger: server error')
|
||||
}
|
||||
}.bind(this)
|
||||
request.open('GET', 'https://' + server + '/.well-known/webfinger' + '?resource=acct:' + name + '@' + server, true)
|
||||
request.send()
|
||||
},
|
||||
// Indicate if the type is compatible with an actor
|
||||
isExpectedActorType: function(actorType) {
|
||||
return (actorType === 'Application') || (actorType === 'Group') || (actorType === 'Organization') || (actorType === 'Person') || (actorType === 'Service')
|
||||
},
|
||||
// Load from the link identifying the account
|
||||
// Callback is a function accepting two arguments:
|
||||
// - a boolean indicating if the loading is complete or in failure,
|
||||
// - a string indicating the failure
|
||||
loadFromProfileUrl: function(profile_url, callback) {
|
||||
this.urls.profile = profile_url
|
||||
// Use ActivityPub protocol to get the user infos
|
||||
var request = new XMLHttpRequest()
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState == 4 && request.status == 200) {
|
||||
var answer = JSON.parse(request.responseText)
|
||||
if (answer && this.isExpectedActorType(answer.type)) {
|
||||
// name and server are previously filled if called from loadFromNameAndServer
|
||||
if (!this.name) {
|
||||
this.name = answer.preferredUsername
|
||||
}
|
||||
if (!this.server) {
|
||||
this.server = profile_url.replace('http://', '').replace('https://', '').split(/[/?#]/)[0]
|
||||
}
|
||||
this.info.display_name = answer.name
|
||||
this.info.summary = answer.summary
|
||||
this.info.icon = answer.icon ? answer.icon.url : undefined
|
||||
this.info.type = answer.type
|
||||
this.urls.outbox = answer.outbox
|
||||
this.urls.inbox = answer.inbox
|
||||
this.urls.followers = answer.followers
|
||||
this.urls.following = answer.following
|
||||
if (answer.endpoints) {
|
||||
this.urls.oauth_token = answer.endpoints.oauthTokenEndpoint
|
||||
this.urls.oauth_registration = answer.endpoints.oauthRegistrationEndpoint
|
||||
this.urls.oauth_authorization = answer.endpoints.oauthAuthorizationEndpoint
|
||||
}
|
||||
this.valid = true
|
||||
callback(true, 'ok')
|
||||
} else {
|
||||
callback(false, 'user profile: incorrect response from server')
|
||||
console.log(answer)
|
||||
}
|
||||
} else if (request.readyState == 4) {
|
||||
callback(false, 'user profile: server error')
|
||||
}
|
||||
}.bind(this)
|
||||
request.open('GET', profile_url, true)
|
||||
request.setRequestHeader('Content-Type', 'application/activity+json')
|
||||
request.setRequestHeader('Accept', 'application/activity+json')
|
||||
request.send()
|
||||
}
|
||||
}
|
||||
|
||||
// Exported structures
|
||||
exports.Actor = Actor
|
|
@ -0,0 +1,145 @@
|
|||
const {Actor} = require('./actor.js')
|
||||
|
||||
// Connected user structure
|
||||
var ConnectedUser = {
|
||||
// Data
|
||||
actor: new Actor(),
|
||||
tokens: {
|
||||
server: {
|
||||
client_id: undefined,
|
||||
client_secret: undefined
|
||||
},
|
||||
user: {
|
||||
refresh_token: undefined,
|
||||
access_token: undefined
|
||||
}
|
||||
},
|
||||
// Methods
|
||||
// Load data from local storage if possible
|
||||
// Callback is used to get the status of the loading
|
||||
loadFromLocalStorage: function (callback) {
|
||||
var server_name = window.localStorage.getItem('last:server.name')
|
||||
var user_name = window.localStorage.getItem('last:user.name')
|
||||
if (server_name && user_name) {
|
||||
ConnectedUser.tokens.server.client_id = window.localStorage.getItem('client_id:' + server_name)
|
||||
ConnectedUser.tokens.server.client_secret = window.localStorage.getItem('client_secret:' + server_name)
|
||||
ConnectedUser.tokens.user.refresh_token = window.localStorage.getItem('refresh_token:' + user_name + '@' + server_name)
|
||||
ConnectedUser.tokens.user.access_token = window.localStorage.getItem('access_token:' + user_name + '@' + server_name)
|
||||
// Load the rest with webfinger / activity pub requests
|
||||
ConnectedUser.actor.loadFromNameAndServer(user_name, server_name, callback)
|
||||
} else {
|
||||
callback(false, 'local storage: no data')
|
||||
}
|
||||
},
|
||||
saveToLocalStorage: function () {
|
||||
window.localStorage.setItem('last:server.name', ConnectedUser.actor.server)
|
||||
window.localStorage.setItem('last:user.name', ConnectedUser.actor.name)
|
||||
window.localStorage.setItem('client_id:' + ConnectedUser.actor.server, ConnectedUser.tokens.server.client_id)
|
||||
window.localStorage.setItem('client_secret:' + ConnectedUser.actor.server, ConnectedUser.tokens.server.client_secret)
|
||||
window.localStorage.setItem('refresh_token:' + ConnectedUser.actor.name + '@' + ConnectedUser.actor.server, ConnectedUser.tokens.user.refresh_token)
|
||||
window.localStorage.setItem('access_token:' + ConnectedUser.actor.name + '@' + ConnectedUser.actor.server, ConnectedUser.tokens.user.access_token)
|
||||
},
|
||||
// Disconnect user
|
||||
disconnect: function() {
|
||||
if (ConnectedUser.actor.name && ConnectedUser.actor.server) {
|
||||
window.localStorage.removeItem('refresh_token:' + ConnectedUser.actor.name + '@' + ConnectedUser.actor.server)
|
||||
window.localStorage.removeItem('access_token:' + ConnectedUser.actor.name + '@' + ConnectedUser.actor.server)
|
||||
}
|
||||
window.localStorage.removeItem('last:server.name')
|
||||
window.localStorage.removeItem('last:user.name')
|
||||
ConnectedUser.actor = new Actor()
|
||||
ConnectedUser.tokens.server.client_id = undefined
|
||||
ConnectedUser.tokens.server.client_secret = undefined
|
||||
ConnectedUser.tokens.user.refresh_token = undefined
|
||||
ConnectedUser.tokens.user.access_token = undefined
|
||||
},
|
||||
// Connection : get tokens from password
|
||||
connect: function(password, callback) {
|
||||
// Retrieve server tokens, or generate them
|
||||
// Then ask for user tokens
|
||||
ConnectedUser.getServerTokens(
|
||||
function(load_ok, failure) {
|
||||
if (load_ok) {
|
||||
ConnectedUser.getUserTokens(password, callback)
|
||||
} else {
|
||||
callback(false, 'OAuth: unable to get client tokens for AP.Mail (' + failure + ')')
|
||||
}
|
||||
})
|
||||
},
|
||||
// Retrieve server tokens
|
||||
getServerTokens: function(callback) {
|
||||
var server = ConnectedUser.actor.server
|
||||
if (server) {
|
||||
// try to load the keys from the localStorage
|
||||
var client_id = window.localStorage.getItem('client_id:' + server)
|
||||
var client_secret = window.localStorage.getItem('client_secret:' + server)
|
||||
if (client_id && client_secret) {
|
||||
ConnectedUser.tokens.server.client_id = client_id
|
||||
ConnectedUser.tokens.server.client_secret = client_secret
|
||||
callback(true, 'ok')
|
||||
} else {
|
||||
// generate keys and store them
|
||||
var request = new XMLHttpRequest()
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState == 4 && request.status == 200) {
|
||||
var answer = JSON.parse(request.responseText)
|
||||
// Retrieve the tokens
|
||||
var client_id = answer.client_id
|
||||
var client_secret = answer.client_secret
|
||||
if (client_id && client_secret) {
|
||||
ConnectedUser.tokens.server.client_id = client_id
|
||||
ConnectedUser.tokens.server.client_secret = client_secret
|
||||
// Save tokens in localStorage
|
||||
window.localStorage.setItem('client_id:' + ConnectedUser.actor.server, ConnectedUser.tokens.server.client_id)
|
||||
window.localStorage.setItem('client_secret:' + ConnectedUser.actor.server, ConnectedUser.tokens.server.client_secret)
|
||||
// Continue
|
||||
callback(true, 'ok')
|
||||
} else {
|
||||
callback(false, 'OAuth: server did not register client.')
|
||||
}
|
||||
} else if (request.readyState == 4) {
|
||||
callback(false, 'OAuth: server error when registering client.')
|
||||
}
|
||||
}
|
||||
request.open('POST', ConnectedUser.actor.urls.oauth_registration, true)
|
||||
var data = new URLSearchParams()
|
||||
data.append('client_name', 'AP.Mail client')
|
||||
data.append('redirect_uris', 'urn:ietf:wg:oauth:2.0:oob')
|
||||
data.append('scopes', 'read write follow')
|
||||
data.append('website', 'http://feuforeve.fr')
|
||||
request.send(data)
|
||||
}
|
||||
} else {
|
||||
callback(false, 'OAuth: unable to retrieve client tokens from an unknown server')
|
||||
}
|
||||
},
|
||||
getUserTokens: function(password, callback) {
|
||||
var request = new XMLHttpRequest()
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState == 4 && request.status == 200) {
|
||||
var answer = JSON.parse(request.responseText)
|
||||
// Retrieve the tokens
|
||||
ConnectedUser.tokens.user.refresh_token = answer.refresh_token
|
||||
ConnectedUser.tokens.user.access_token = answer.access_token
|
||||
// Save the tokens
|
||||
ConnectedUser.saveToLocalStorage()
|
||||
// OK, return to callback
|
||||
callback(true, 'ok')
|
||||
} else if (request.readyState == 4) {
|
||||
callback(false, 'OAuth: cannot get user tokens')
|
||||
}
|
||||
}
|
||||
request.open('POST', ConnectedUser.actor.urls.oauth_token, true)
|
||||
var data = new URLSearchParams()
|
||||
data.append('client_id', ConnectedUser.tokens.server.client_id)
|
||||
data.append('client_secret', ConnectedUser.tokens.server.client_secret)
|
||||
data.append('grant_type', 'password')
|
||||
data.append('username', ConnectedUser.actor.name)
|
||||
data.append('password', password)
|
||||
data.append('scope', 'read write follow')
|
||||
request.send(data)
|
||||
}
|
||||
}
|
||||
|
||||
// Exported structures
|
||||
exports.ConnectedUser = ConnectedUser
|
|
@ -0,0 +1,29 @@
|
|||
const {Actor} = require('./actor.js')
|
||||
|
||||
// A cache for actors, to avoid loading same actors several times when loading timelines
|
||||
var KnownActors = {
|
||||
actors: {},
|
||||
get: function(profile) {
|
||||
return KnownActors.actors[profile]
|
||||
},
|
||||
set: function(profile, actor) {
|
||||
KnownActors.actors[profile] = actor
|
||||
},
|
||||
// Retrieve an actor
|
||||
retrieve: function(profile, callback) {
|
||||
if (KnownActors.get(profile)) {
|
||||
callback(true, 'ok')
|
||||
} else {
|
||||
var actor = new Actor()
|
||||
actor.loadFromProfileUrl(
|
||||
profile,
|
||||
function(load_ok, failure_message) {
|
||||
KnownActors.set(profile, actor) // in case of failure, actor is not valid
|
||||
callback(load_ok, failure_message)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exported structures
|
||||
exports.KnownActors = KnownActors
|
|
@ -0,0 +1,99 @@
|
|||
const {ConnectedUser} = require('./connected-user.js')
|
||||
|
||||
// Outgoing messages
|
||||
var Message = function() {
|
||||
this.to = []
|
||||
this.cc = []
|
||||
}
|
||||
Message.prototype = {
|
||||
// Attributes
|
||||
to: [],
|
||||
cc: [],
|
||||
public_visibility: 'to', // 'to', 'cc', 'non'
|
||||
follower_visibility: 'cc', // 'to', 'cc', 'non'
|
||||
subject: '',
|
||||
content: '',
|
||||
type: 'Note',
|
||||
media_type: 'text/plain', // Pleroma also accepts: text/markdown, text/html, text/bbcode
|
||||
// Methods
|
||||
setContent: function(subject, content) {
|
||||
this.subject = subject
|
||||
this.content = content
|
||||
},
|
||||
setVisibility: function(pub, follower) {
|
||||
// TODO: filter visibility
|
||||
this.public_visibility = pub
|
||||
this.follower_visibility = follower
|
||||
},
|
||||
// Add recipients
|
||||
addToRecipient: function(actor) {
|
||||
this.to.push(actor)
|
||||
},
|
||||
addCcRecipient: function(actor) {
|
||||
this.cc.push(actor)
|
||||
},
|
||||
// Remove recipients
|
||||
removeToRecipient: function(actor) {
|
||||
var idx = this.to.findIndex(x => x.urls.profile === actor.urls.profile)
|
||||
if (idx !== -1) {
|
||||
this.to.splice(idx, 1)
|
||||
}
|
||||
},
|
||||
removeCcRecipient: function(actor) {
|
||||
var idx = this.cc.findIndex(x => x.urls.profile === actor.urls.profile)
|
||||
if (idx !== -1) {
|
||||
this.cc.splice(idx, 1)
|
||||
}
|
||||
},
|
||||
// Send message
|
||||
send: function(callback) {
|
||||
// Recipients
|
||||
var recipients_to = this.to.map(x => x.urls.profile)
|
||||
var recipients_cc = this.cc.map(x => x.urls.profile)
|
||||
if (this.public_visibility === 'to') {
|
||||
recipients_to.push('https://www.w3.org/ns/activitystreams#Public')
|
||||
} else if (this.public_visibility === 'cc') {
|
||||
recipients_cc.push('https://www.w3.org/ns/activitystreams#Public')
|
||||
}
|
||||
if (this.follower_visibility === 'to') {
|
||||
recipients_to.push(ConnectedUser.actor.urls.followers)
|
||||
} else if (this.follower_visibility === 'cc') {
|
||||
recipients_cc.push(ConnectedUser.actor.urls.followers)
|
||||
}
|
||||
//
|
||||
var message = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: this.type,
|
||||
to: recipients_to,
|
||||
cc: recipients_cc,
|
||||
summary: this.subject !== '' ? this.subject : null,
|
||||
content: this.content,
|
||||
mediaType: this.media_type
|
||||
}
|
||||
// Encapsulate in a create activity, although this should not be mandatory
|
||||
var activity = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'Create',
|
||||
to: message.to,
|
||||
cc: message.cc,
|
||||
object: message
|
||||
}
|
||||
var json_message = JSON.stringify(activity)
|
||||
var request = new XMLHttpRequest()
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState == 4 && request.status == 201) {
|
||||
callback(true, 'ok')
|
||||
} else if (request.readyState == 4) {
|
||||
callback(false, 'Send: Message not created on server')
|
||||
}
|
||||
}
|
||||
request.open('POST', ConnectedUser.actor.urls.outbox, true)
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + ConnectedUser.tokens.user.access_token)
|
||||
request.setRequestHeader('Content-Type', 'application/activity+json')
|
||||
request.setRequestHeader('Accept', 'application/activity+json')
|
||||
request.send(json_message)
|
||||
}
|
||||
}
|
||||
|
||||
// Exported structures
|
||||
exports.Message = Message
|
|
@ -0,0 +1,60 @@
|
|||
const {Activity} = require('./activity.js')
|
||||
|
||||
// Timeline class
|
||||
// Represent a collection of activities
|
||||
var Timeline = function() {}
|
||||
Timeline.prototype = {
|
||||
load: function(url, token, callback) {
|
||||
var request = new XMLHttpRequest()
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState == 4 && request.status == 200) {
|
||||
var answer = JSON.parse(request.responseText)
|
||||
if (answer.type === 'OrderedCollection' && answer.first && typeof answer.first === 'string') {
|
||||
// First is a link => load again with the first
|
||||
this.load(answer.first, token, callback)
|
||||
} else if (answer.type === 'OrderedCollection' && answer.first && answer.first.type === 'OrderedCollectionPage') {
|
||||
this.activities = []
|
||||
this.prev = answer.first.prev
|
||||
this.next = answer.first.next
|
||||
this.parseActivities(answer.first.orderedItems, callback)
|
||||
} else if (answer.type === 'OrderedCollectionPage') {
|
||||
this.activities = []
|
||||
this.prev = answer.prev
|
||||
this.next = answer.next
|
||||
this.parseActivities(answer.orderedItems, callback)
|
||||
} else {
|
||||
callback(false, 'Timeline: unexpected answer from server')
|
||||
console.log(answer)
|
||||
}
|
||||
} else if (request.readyState == 4) {
|
||||
callback(false, 'Timeline: server error')
|
||||
}
|
||||
}.bind(this)
|
||||
request.open('GET', url, true)
|
||||
if (token) {
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + token)
|
||||
}
|
||||
request.setRequestHeader('Content-Type', 'application/activity+json')
|
||||
request.setRequestHeader('Accept', 'application/activity+json')
|
||||
request.send()
|
||||
},
|
||||
parseActivities: function(raw_activities, callback) {
|
||||
// Get the next activity
|
||||
var raw_act = raw_activities.shift()
|
||||
if (raw_act) {
|
||||
var act = new Activity(raw_act)
|
||||
act.loadActors(
|
||||
function(load_ok, failure_message) {
|
||||
if (load_ok) {
|
||||
this.activities.push(act)
|
||||
}
|
||||
this.parseActivities(raw_activities, callback)
|
||||
}.bind(this))
|
||||
} else {
|
||||
callback(true, 'ok')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exported structures
|
||||
exports.Timeline = Timeline
|
Loading…
Reference in New Issue