diff --git a/publish-to-gts/info.json b/publish-to-gts/info.json
new file mode 100644
index 0000000..a442da1
--- /dev/null
+++ b/publish-to-gts/info.json
@@ -0,0 +1,10 @@
+{
+ "name": "Publish to GoToSocial",
+ "identifier": "publish-to-gts",
+ "script": "publish-to-gts.qml",
+ "authors": ["@77nnit"],
+ "platforms": ["linux", "windows"],
+ "version": "0.1.1",
+ "minAppVersion": "24.11.0",
+ "description" : "Publish To GtS lets you publish your notes to your GoToSocial account."
+}
diff --git a/publish-to-gts/publish-to-gts.qml b/publish-to-gts/publish-to-gts.qml
new file mode 100644
index 0000000..21c7a8e
--- /dev/null
+++ b/publish-to-gts/publish-to-gts.qml
@@ -0,0 +1,379 @@
+import QtQml 2.0
+import QOwnNotesTypes 1.0
+
+Script {
+ property string serverInstance;
+ property string authCode;
+ property string visibility;
+ property bool local_only;
+ property bool sensitive;
+ property string spoiler_text;
+ property string language;
+
+ property variant settingsVariables: [
+ {
+ "identifier": "serverInstance",
+ "name": "Server instance",
+ "description": "Server instance you want to connect to (i.e.: example.org - no spaces, no protocol, no slashes)",
+ "type": "string",
+ "default": "example.org",
+ },
+ {
+ "identifier": "authCode",
+ "name": "Authentication Code",
+ "description": "Code returned by GtS after performing a successful authentication, if you paste it here you won't need to authenticate again until expiry",
+ "type": "string-secret",
+ "default": "",
+ },
+ {
+ "identifier": "visibility",
+ "name": "Visibility",
+ "description": "Default visibility for published posts",
+ "type": "selection",
+ "default": "public",
+ "items": {"public": "Public", "unlisted": "Unlisted", "private": "Private", "mutuals_only": "Mutuals", "direct": "Direct Message"},
+ },
+ {
+ "identifier": "local_only",
+ "name": "Local only",
+ "description": "If the post is local only, it will not be seen from federated instances",
+ "text": "Yes, let it be Local Only",
+ "type": "boolean",
+ "default": false,
+ },
+ {
+ "identifier": "sensitive",
+ "name": "Content Warning",
+ "description": "The post text will not be immediately visible, as it may be sensible to some audience",
+ "text": "Yes, the post content is usually sensible",
+ "type": "boolean",
+ "default": false,
+ },
+ {
+ "identifier": "spoiler_text",
+ "name": "Content Warning text",
+ "description": "Text to show as a content warning for senstitive posts",
+ "type": "string",
+ "default": "Sensible content ahead!",
+ },
+ {
+ "identifier": "language",
+ "name": "Language code",
+ "description": "2-chars laguage code as per https://www.loc.gov/standards/iso639-2/php/English_list.php:",
+ "type": "string",
+ "default": "en",
+ },
+ ];
+
+ property variant currentParams: {
+ "visibility": null,
+ "local_only": null,
+ "sensitive": null,
+ "spoiler_text": null,
+ "language": null
+ }
+ property variant additionalParams: {
+ "created_at": null,
+ "id": null,
+ "url": null
+ }
+
+ function init() {
+ script.registerCustomAction("publish", "Publish current note to GtS","",true,true,false);
+ script.registerCustomAction("newPost", "New post for GtS","",true,true,false);
+
+ //validate server instance
+ if (!(script.getPersistentVariable("publishToGts/"+serverInstance))){
+ serverInstance = serverInstance.match(/(?!(\w+:\/\/))(\w+.)*(\w+)/g)[0];
+ }
+ }
+
+ // This function returns a true or a false or a string that doesn't match true or false
+ function parseBool(val) {
+ if (Object.prototype.toString.call(val)=="[object String]"){
+ return val == "true" ? true : (val == "false" ? false : val);
+ } else {
+ return val === true || val === "true";
+ }
+ }
+
+ // This function generates a Post Header with comments and default parameter values for the current note.
+ // if includeAdditional = true adds additional attributes (like created_at for already published notes)
+ function generatePostHeader(includeAdditional){
+ let postHeader = "";
+ postHeader += "***Publish to GtS - Post Header***\n";
+ postHeader += "\n";
+ postHeader += "#Edit the values to adjust the post settings.\n";
+ postHeader += "#Missing properties will be defaulted as per script settings.\n";
+ postHeader += "#Confirmation will be asked before publishing.\n";
+ postHeader += "#This section will not be published.\n";
+ postHeader += "\n";
+ // checking all post parameters in currentParams against user settings params
+ Object.keys(currentParams).forEach(function(key){
+ // using .every to break out the cycle
+ settingsVariables.every(function(varObj){
+ if (key == varObj.identifier){
+ // the eval used here is safe, as the variable it evaluates contains always a string value.
+ postHeader += `${key}: ${eval(varObj.identifier)}${"\n"}`;
+ return false;
+ } else {
+ return true;
+ }
+ });
+ });
+ if (includeAdditional){
+ Object.keys(additionalParams).forEach(function(key){
+ postHeader += `${key}: ${additionalParams[key]}${"\n"}`;
+ });
+ }
+ return postHeader;
+ }
+
+ function updatePostHeader(){
+ let current = script.currentNote()
+ let sections = current.noteText.split("---");
+ script.tagCurrentNote("Pub2GtS");
+ if (sections && sections[1]){
+ script.triggerMenuAction("actionAllow_note_editing", 1);
+ mainWindow.focusNoteTextEdit();
+ script.noteTextEditSetCursorPosition(0);
+ script.noteTextEditSelectAll();
+ script.noteTextEditWrite([sections[0], ("---\n" + generatePostHeader(true) + "\n---")].concat(sections.slice(2)).join(""));
+ }
+ }
+
+ // function that reads a Post Header section and populate the currentParams object
+ function decodePostHeader(){
+ let current = script.currentNote(); // current note
+ let postParams = {};
+ let postHeader = current.noteText.split("---");
+ if (postHeader[1]){
+ postHeader[1].split("\n").forEach(function(param){
+ if (! param.startsWith("#")){
+ let thisParam = param.split(":");
+ if (thisParam[0] && thisParam[1]){
+ postParams[thisParam[0].trim()]=param.split(":").slice(1).join("").trim();
+ }
+ }
+ });
+ currentParams = postParams;
+ return true;
+ } else {
+ script.log("Publish to GtS: Current note does not have a valid Post Header.");
+ return false;
+ }
+ }
+
+ // This function shows a confirmation message box with current note posting parameters
+ function confirmPublish(){
+ let msgText = "You are about to publish the current note with the following settings:\n
";
+ Object.keys(currentParams).forEach(function(key){
+ msgText += "- " + key + ": " + currentParams[key] + "
";
+ });
+ msgText += "
";
+ msgText += `Press "Ok" to confirm and post the note with the above settings;`
+ msgText += `Press "Cancel" to continue editing your note.`;
+ return (script.questionMessageBox(msgText, "Publish to GtS: confirm action", 0x00000400|0x00400000) == 1024);
+ }
+
+ // This function ask user confirmation to generate a postHeader for the current post
+ function confirmPostHeader(){
+ let msgText = `The current note does not appear to have a valid Post Header for publishing. You can:`;
+ msgText += `- Press "OK" to publish the note with default settings
`;
+ msgText += `- Press "Apply" to cancel the publishing and generate a Post Header for your note
`;
+ msgText += `- Press "Cancel" to cancel the publishing and add a Post Header manually
`;
+ msgText += `Note:Generating a Post Header will add the header above the title, if present.`;
+ return script.questionMessageBox(msgText, "Publish to GtS: missing Post Header", 0x00000400|0x02000000|0x00400000);
+ }
+
+ function confirmDuplicatePost(){
+ let msgText = `The current note Post Header contains "created_at" property. This may indicate that the note was already published. Are you sure you want to publish the note again?`;
+ return (script.questionMessageBox(msgText, "Publish to GtS: publish duplicate note", 0x00000400|0x00400000)==1024);
+ }
+
+ // This function performs all API endpoint requests and returns text responses
+ function request(verb, endpoint, aT, data){
+ // create request
+ let xhr = new XMLHttpRequest();
+ let url = "https://" + serverInstance + endpoint;
+ // open synchronous request
+ xhr.open(verb, url, false);
+ // setting content type request header
+ xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
+ // is accessToken is provided in the function call adding the Authorization header
+ if (aT && aT.length > 0){
+ xhr.setRequestHeader("Authorization", "Bearer " + aT);
+ }
+ // sending the request with data, if date is there
+ if (data){
+ // xhr.setRequestHeader("Content-Length", JSON.stringify(data).length);
+ xhr.send(JSON.stringify(data));
+ } else {
+ xhr.send();
+ }
+ // since tha call is synchronous at this poit we already have the response
+ // the response body is returned
+ if (xhr.status == 200){
+ return xhr.response;
+ // If the request is nor completed with a success code 200, write to che console the error and return null
+ } else {
+ script.log ("Publish to GtS: Error: " + serverInstance + endpoint + " returned code " + xhr.status + " - " + xhr.statusText);
+ script.log (JSON.stringify(xhr.response));
+ script.log (xhr.getAllResponseHeaders());
+ return null;
+ }
+ }
+
+ // This function wraps the "verify credential" call
+ function verifyCredentials(aT){
+ // calling the request over the specific endpoint, passing the accessToken
+ let res = request ("GET", "/api/v1/accounts/verify_credentials", aT);
+ return res && true;
+ }
+
+ function customActionInvoked(identifier) {
+
+ //handler for newPost command
+ if (identifier == "newPost"){
+ let date = new Date();
+ let headline = "Note " + date.toISOString();
+ script.createNote("# " + headline + "\n\n---\n\n" + generatePostHeader() + "\n\n---\n\n");
+ let currentNote = script.currentNote();
+ script.triggerMenuAction("actionAllow_note_editing", 1);
+ currentNote.renameNoteFile(headline);
+ mainWindow.focusNoteTextEdit();
+ script.tagCurrentNote("Pub2GtS");
+ return;
+ }
+
+ // handler for publish command
+ if (identifier == "publish") {
+ // local variables init
+ let clientName = "QONPublishToGts";
+ let clientMode = "Read+Write";
+ let clientId = "";
+ let clientSecret = "";
+ let credentialsVerified = false;
+ let current = script.currentNote(); // current note
+ let currentPost = ""; //current part of note that represents a post when post header stripped
+
+ if (!decodePostHeader()){
+ let exitCondition = true;
+ let confirmResult = confirmPostHeader();
+ switch (confirmResult){
+ case 1024:{
+ script.log("Publish to GtS: default posting settings confirmed.");
+ exitCondition = false;
+ break;
+ }
+ case 33554432:{
+ script.log("Publish to GtS: generating default Post Header.");
+ // attach Post Header at the beginning of post
+ script.triggerMenuAction("actionAllow_note_editing", 1);
+ mainWindow.focusNoteTextEdit();
+ script.noteTextEditSetCursorPosition(0);
+ let date = new Date();
+ let headline = "Note " + date.toISOString();
+ script.noteTextEditWrite("# " + headline + "\n\n---\n\n" + generatePostHeader() + "\n\n---\n\n");
+ script.tagCurrentNote("Pub2GtS");
+ exitCondition = true;
+ break;
+ }
+ default:{
+ script.log("Publish to GtS: publishing with current settings canceled.");
+ exitCondition = true;
+ break;
+ }
+ }
+ if (exitCondition) return;
+ }
+ if (currentParams.created_at){
+ script.log("Publish to Gts: actual note may have already been published.");
+ if (!confirmDuplicatePost()){
+ return;
+ }
+ }
+ if (confirmPublish()){
+ script.log("Publish to GtS: note publishing confirmed.");
+ script.tagCurrentNote("Pub2GtS");
+ } else {
+ script.log("Publish to GtS: note publishing canceled.");
+ return;
+ }
+
+ // recover accessToken from persistent variables, if present
+ let accessToken = script.getPersistentVariable("publishToGts/"+authCode);
+ // check if accessToken was found on the persistent variables
+ if (accessToken && accessToken.length > 0){
+ // access token was found, verifying credentials
+ credentialsVerified = verifyCredentials(accessToken);
+ } else {
+ let registerResponse = request("POST", "/api/v1/apps", "", {"client_name":clientName,"redirect_uris":"urn:ietf:wg:oauth:2.0:oob","scopes":clientMode});
+ if (!registerResponse){
+ return;
+ }
+ clientId = JSON.parse(registerResponse).client_id;
+ clientSecret = JSON.parse(registerResponse).client_secret;
+
+ // need to show a window with a http link for the user to click
+ let authAddress = `https://${serverInstance}/oauth/authorize?client_id=${clientId}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=${clientMode}`;
+ let popupTitle = "GoToSocial Authentication";
+ let popupText = `- Visit this link
- Paste your code below once authenticated
`;
+ //waiting for user to insert the authorzation code
+ authCode = script.inputDialogGetText(popupTitle, popupText, "Authorization code here");
+ if (!authCode){
+ script.log("No authorization code entered.");
+ return;
+ }
+ script.log("authorization code inserted: " + authCode);
+ // exchanging for token
+ let tokenRequest = request("POST", "/oauth/token", "", {"redirect_uri": "urn:ietf:wg:oauth:2.0:oob","client_id": clientId, "client_secret": clientSecret,"grant_type": "authorization_code","code": authCode});
+ script.log(tokenRequest);
+ if (!tokenRequest){
+ script.log("Unable to get access Token.");
+ return;
+ } else {
+ accessToken = JSON.parse(tokenRequest).access_token;
+ credentialsVerified = verifyCredentials(accessToken);
+ }
+ }
+ if (credentialsVerified){
+ script.log("Credentials verified!");
+ //saving accessToken in a persistent variable called with the same name as the authCode
+ script.setPersistentVariable("publishToGts/"+authCode, accessToken);
+ } else {
+ script.log("Credentials verification failed. Check the server or the internet connection and retry!");
+ return;
+ }
+ // We can proceed with posting the actual note
+ // Getting the current note markdown
+
+ let noteSections = current.noteText.split("---");
+ // posting only if text is present, excluding the Post Header, delimited by ---
+ if (noteSections.length > 2 && /./gm.test(noteSections[2])){
+ currentPost = noteSections[2];
+ } else {
+ script.log("Publish to GtS: Current note does not have a text to be published.")
+ return;
+ }
+ //populating the status object with status and post parameters
+ let status = {
+ "status": currentPost
+ };
+
+ Object.keys(currentParams).forEach(function(param){
+ status[param] = parseBool(currentParams[param]);
+ });
+
+ let statusResult = JSON.parse(request ("POST", "/api/v1/statuses", accessToken, status));
+ if (statusResult && statusResult["created_at"]){
+ additionalParams["created_at"] = statusResult["created_at"];
+ additionalParams["id"] = statusResult["id"];
+ additionalParams["url"] = statusResult["url"];
+ updatePostHeader();
+ script.tagCurrentNote("Published");
+ }
+ return;
+ }
+ }
+}
diff --git a/publish-to-gts/readme.md b/publish-to-gts/readme.md
new file mode 100644
index 0000000..1609199
--- /dev/null
+++ b/publish-to-gts/readme.md
@@ -0,0 +1,85 @@
+## Overview
+
+The **Publish To GoToSocial** script for QOwnNotes allows users to publish markdown notes directly to their GoToSocial activitypub instance.
+Future versions will be also able to download posts, to keep them as notes or to use other script for publishing them as standalone websites.
+
+## Manual Installation
+
+1. **Download the Plugin**: Save the `publish-to-gts.qml` file to your local machine.
+2. **Add to QOwnNotes**:
+ - Open QOwnNotes.
+ - Navigate to `Settings` > `Scripting`.
+ - Click on `Add script... > Add local script`.
+ - Select the `publish-to-gts.qml` file in the script folder.
+3. **Activate the Plugin**:
+ - Go back to QOwnNotes.
+ - In the `Scripting` settings, ensure that `publish-to-gts.qml` is listed and checked.
+
+## Settings
+
+- **Server instance**: Server instance you want to connect to (i.e.: example.org - no spaces, no protocol, no slashes);
+- **Authentication Code**: Code returned by GtS after performing a successful authentication, if you paste it here you won't need to authenticate again until expiry;
+- **Visibility**: Default visibility for published posts:
+ - Public
+ - Unlisted
+ - Private
+ - Mutuals
+ - Direct Message
+- **Local Only**: If the post is local only, it will not be seen from federated instances;
+- **Content Warning**: The post text will not be immediately visible, as it may be sensible to some audience;
+- **Content Warning text**: Text to show as a content warning for senstitive posts;
+- **Language code**: 2-chars laguage code as per https://www.loc.gov/standards/iso639-2/php/English_list.php.
+
+## Usage
+
+After installation type the server instance name on the script settings and press Ok. Connection with your server will be established the first time you will publish a post.
+
+There are two implemented use cases:
+1. **Creation of a new note**
+2. **Note publishing**
+
+### Creation of a new note
+
+1. Select `Custom actions > New Post for GtS` on the context menu **or** click on `Scripting > Custom actions > New post for GtS`;
+2. A new note with default Post Header will be created
+3. Write your post below the front matter, adjust the post settings as per your preferences. You can try to add other accepted parameters and they should work, but it's not a supported feature. Not sure for nested object parameters.
+
+### Note publishing for first access
+
+1. On the note to be published right click and select `Custom actions > Publish current note to GtS`
+2. A *"Publish to Gts: confirm action"* dialog pops out, summarizing the post settings and asking for confirmation. Press Ok.
+3. If this is the first note published a *"GoToSocial Authentication"* input dialog pops out: ppen the link referenced in the popup on your browser (you may need to copy and paste it depending on your OS).
+4. Perform the authentication on your instance with the user you want to impersonate and click on *"Allow"* in the confirmation page
+5. Copy the *"Authorization Code"* from the web page and paste it back to the *"GoToSocial Authentication"* popup. You may also want to paste the same code on your script settings page, on the **Authorization Code** parameter, so that you won't need re-authenticate when you opena new session on QON.
+6. The post gets published with current settings.
+
+#### Other Note publishing details
+
+A published note gains extra information on the front matter:
+- a **created_at** datatime field, that is returned by your server
+- an **id** as the published post id
+- an **url** as the published post permalink
+
+A published note also gains 2 note tags:
+- a `Pub2GtS` tag to identify the post as managed and modified by the publish-to-gts script
+- a `Published` tag to identify a note that has been already published
+
+In case you try to publish a note that contains `created_at` in the front matter a confirmation will be requested. If confirmed the post will be published again, updating the `created_at` datetime and the `id` and `url`, as GtS do not provide yet a post editing feature.
+
+In case you try to publish a note that does not contain the front matter, a front matter will be generated at the moment and updated when the note is published.
+
+## Contributing
+
+If you have suggestions or improvements, feel free to fork the repository and submit a pull request.
+
+## Needed testers on MacOS!
+
+## ToDo
+
+## License
+
+This project is licensed under the MIT License. See the `LICENSE` file for details.
+
+---
+
+Enjoy using the Publish To GtS script to enhance your social publishing experience with QOwnNotes!