+
+/* A A# B C C# D D# E F F# G G# */
+/* A Bb B C Db D Eb E F Gb G Ab */
+/* 0 1 2 3 4 5 6 7 8 9 10 11 */
+
+/* STRINGS ARE: E=0, A=1, D=2, G=3, B=4, E=5 IN ORDER */
+
+// This can be reset for alternate tunings, this is EADGBE tuning
+// here:
+
+ int openStrings[NUM_STRINGS] = { 7, 0, 5, 10, 2, 7 };
+
+static char *strNotes[] = { "A ", "A#", "B ", "C ", "C#", "D ",
+ "D#", "E ", "F ", "F#", "G ", "G#" };
+
+//
+// This class contains a decode chord, and can take a string and turn
+// it into a chord. A chord is a triad or greater here. The code is
+// decoded from a string into notes in the c array (notes: are A=0, A#=1,
+// and so on. The longest chord you can possibly have is 12 notes which
+// would be every note in an octave, since chords are typical 3 to 5
+// notes, remaining notes in the c[] array are set to -1
+//
+class Chord
+{
+ public:
+ // String set to last error
+ char errorStr[64];
+
+ // Notes that are in the chord -1 should be ignored
+ int notes[12];
+
+ // NOtes that are optional have a 1, otherwise a zero
+ int optional[12];
+
+ // ------------------ MEMBER FUNCTIONS ----------------------------
+
+ // Clears last chord
+ void clear();
+
+ // Decodes input string into notes[] array, return error string
+ int findChord(char *s);
+
+ // Print last error message from chord parse
+ void printError();
+
+ // Prints out notes[] array to screen
+ void print(char *chordName);
+
+ // Is the note note in the chord
+ int inChord(int note);
+
+ // Is this chord covered by the array of notes (ie: Does the
+ // array contain all the notes of the chord
+ int covered(int *noteArray);
+
+ // Return the root note of the chord
+ int getRoot() { return notes[0]; }
+
+ // Given a chord string, return the note that starts it (and recognize
+ // sharps and flats (b and #)
+ int get_base(char *s);
+
+ // Get a note offset from another...
+ int note_offset(int base, int offset) {
+ if((! base) && (offset == (-1)))
+ return(11);
+ return ((base + offset) % 12);
+ }
+} ;
+
+//
+// This class holds a fingering for a chord, as well as a score for that
+// fingering. This class also keeps the CHORDSTACK (ie: 100) best version
+// of that chord to print out in sorted order (best score 1st).
+//
+class Fretboard
+{
+ public:
+
+ // Score of current chord, held in an array so we can track the
+ // components of the score (see the SCORE_ defines in the begining
+ // of this file
+ int score[NUMSCORETYPES];
+
+ // Fretboard of the current chord, fretboard[0] is the low (bass) E
+ // string. A fretboard value of 0 is an open string, -1 is an
+ // X'ed out string (not strung)
+ int fretboard[NUM_STRINGS];
+
+ // Notes of the current fretboard fingering for a chord
+ int notes[NUM_STRINGS];
+
+ // The best fret layouts so far based on score
+ int bestFrets[CHORDSTACK][NUM_STRINGS];
+
+ // The best fret layout note sets so far based on score
+ int bestNotes[CHORDSTACK][NUM_STRINGS];
+
+ // The best scores
+ int bestScores[CHORDSTACK][NUMSCORETYPES];
+
+ // Keep track of stack sDepth to speed it up!
+ int sDepth;
+
+ // ------------------ MEMBER FUNCTIONS ----------------------------
+
+ // Construct one
+ Fretboard();
+
+ // Given a chord (and the current fretboard state) score this doggie
+ // and leave the score value in score
+ void getScore(Chord &chord);
+
+ // Print the current Fretboard state (not the stack)
+ void print();
+
+ // Print the fretboard stack
+ void printStack();
+
+ // Iterate over different fretboard variations for a chord
+ void iterate(Chord &chord);
+
+ // Take the current fretboard state and put it into the stack
+ void addToBest();
+
+ // Get the span of the current chord, excluding open string
+ int getSpan();
+
+ // Get the span of the current chord, INCLUDING open string
+ int getTotalSpan();
+};
+
+//
+// Before building a chord, clear the notes and optional arrays
+//
+void
+Chord::clear()
+{
+ for (int i = 0; i < 12; i++)
+ {
+ notes[i] = -1;
+ optional[i] = 0;
+ }
+}
+
+//
+// Print out the last error string
+//
+void
+Chord::printError()
+{
+ printf("Error: %s\n", errorStr);
+}
+
+//
+// I dunno, our C++ compiler at work did not have strupr, so heres mine
+//
+void
+myStrupr(char *s)
+{
+ while (*s)
+ {
+ if (islower(*s))
+ *s = toupper(*s);
+ s++;
+ }
+}
+
+//
+// Decodes input string into notes[] array, return error string
+// always: 0 = root, 1 = 3rd, 2 = 5th
+// Also sets the optional array in parallel with the notes[]
+//
+int
+Chord::findChord(char *s)
+{
+ clear();
+
+ // up case the string
+ myStrupr(s);
+
+ // DECODE ROOT NOTE : A - G
+ notes[0] = get_base(s);
+ s++;
+
+ // CHECK FOR SHARP OR FLAT ROOT NOTE
+ if (*s == '#') s++;
+ if (*s == 'B') s++;
+
+ // MODIFY THE ROOT BY M, MIN, SUS2, SUS4, or diminished
+ if (!strncmp(s, "MIN", 3 ))
+ {
+ notes[1] = note_offset(notes[0], 3);
+ s += 3;
+ optional[2] = 1;
+ }
+ else if (!strncmp(s, "MAJ", 3))
+ {
+ // Do nothing, but stops program from seeing the
+ // first m in maj as a minor (see next line)...so give a normal 3rd
+ notes[1] = note_offset(notes[0], 4);
+ optional[2] = 1;
+ }
+ else if (!strncmp(s, "M", 1))
+ {
+ notes[1] = note_offset(notes[0], 3);
+ s += 1;
+ optional[2] = 1;
+ }
+ else if (!strncmp(s, "SUS", 1))
+ {
+ s += 3; // go past sus
+ if (*s == '2')
+ notes[1] = note_offset(notes[0], 2);
+ else if (*s == '4')
+ notes[1] = note_offset(notes[0], 5);
+ else
+ {
+ strcpy(errorStr, "sus must be followed by 2 or 4");
+ return 1;
+ }
+ s++; // Go past 2 or 4
+ optional[2] = 1;
+ }
+ else if ((!strncmp(s, "DIM", 3 )) && (!isdigit(s[3])))
+ {
+ // If it is diminished, just return (no other stuff allowed)/
+ notes[1] = note_offset(notes[0], 3);
+ notes[2] = note_offset(notes[0], 6);
+ notes[3] = note_offset(notes[0], 9);
+ return 0;
+ }
+ else if ((!strncmp(s, "AUG", 3 )) && (!isdigit(s[3])))
+ {
+ // If it is diminished, just return (no other stuff allowed)/
+ notes[1] = note_offset(notes[0], 4);
+ notes[2] = note_offset(notes[0], 8);
+ return 0;
+ }
+ else
+ {
+ notes[1] = note_offset(notes[0], 4);
+ // optional[1] = 1;
+ // optional[2] = 1;
+ }
+
+ notes[2] = note_offset(notes[0], 7);
+
+
+ // At this point, the 1,3,5 triad or variant is built, now add onto
+ // it until the string end is reached...
+ // Next note to add is index = 3...
+ int index = 3;
+ enum homeboy { NORMAL, MAJ, ADD, AUG, DIM } mtype ;
+ char lbuf[10];
+
+ while (*s)
+ {
+ // FIrst, check the mtype of modifier, ie: Aug, Maj, etc...
+ mtype = NORMAL;
+ if (!strncmp(s, "MAJ", 3))
+ {
+ mtype = MAJ;
+ s += 3;
+ }
+ else if (!strncmp(s, "ADD", 3))
+ {
+ mtype = ADD;
+ s += 3;
+ }
+ else if (!strncmp(s, "AUG", 3))
+ {
+ mtype = AUG ;
+ s += 3;
+ }
+ else if (!strncmp(s, "DIM", 3))
+ {
+ mtype = DIM;
+ s += 3;
+ }
+ else if ( *s == '+' )
+ {
+ mtype = AUG;
+ s += 1;
+ }
+ else if ( *s == '-' )
+ {
+ mtype = DIM;
+ s += 1;
+ }
+ else if ( *s == '#' )
+ {
+ mtype = AUG;
+ s += 1;
+ }
+ else if ( *s == 'B' )
+ {
+ mtype = DIM;
+ s += 1;
+ }
+ else if ( *s == '/' )
+ {
+ mtype = ADD;
+ s += 1;
+ }
+ // Now find the number...
+ if (isdigit(*s))
+ {
+ lbuf[0] = *s++;
+ lbuf[1] = '\0';
+ }
+ else
+ {
+ sprintf(errorStr, "Expecting number, got %s", s);
+ return 1;
+ }
+ // 2nd digit?
+ if (isdigit(*s))
+ {
+ lbuf[1] = *s++;
+ lbuf[2] = '\0';
+ }
+
+ int number = atoi(lbuf);
+
+ switch (number)
+ {
+ case 7 :
+ notes[index] = note_offset(notes[0], 10);
+ break;
+ case 9 :
+ notes[index] = note_offset(notes[0], 2);
+
+ // put the 7th in 2nd so it can be maj'ed if need be...
+ if ((mtype == NORMAL) || (mtype == MAJ))
+ {
+ index++;
+ notes[index] = note_offset(notes[0], 10);
+ optional[index] = 1; // 7th is optional, unless it is maj!
+ }
+
+ break;
+ case 11 :
+ notes[index] = note_offset(notes[0], 5);
+
+ // put the 7th in 2nd so it can be maj'ed if need be...
+ if ((mtype == NORMAL) || (mtype == MAJ))
+ {
+ index++;
+ notes[index] = note_offset(notes[0], 10);
+ optional[index] = 1; // 7th is optional, unless it is maj!
+ }
+
+ break;
+ case 13 :
+ notes[index] = note_offset(notes[0], 9);
+ index++;
+ notes[index] = note_offset(notes[0], 5);
+ optional[index] = 1; // 7th is optional, unless it is maj!
+ index++;
+ notes[index] = note_offset(notes[0], 2);
+ optional[index] = 1; // 7th is optional, unless it is maj!
+
+ // put the 7th in 2nd so it can be maj'ed if need be...
+ if ((mtype == NORMAL) || (mtype == MAJ))
+ {
+ index++;
+ notes[index] = note_offset(notes[0], 10);
+ optional[index] = 1; // 7th is optional, unless it is maj!
+ }
+
+ break;
+ case 2:
+ notes[index] = note_offset(notes[0], 2);
+ break;
+ case 4:
+ notes[index] = note_offset(notes[0], 5);
+ break;
+ case 6:
+ notes[index] = note_offset(notes[0], 9);
+ break;
+ case 5:
+ notes[index] = note_offset(notes[0], 7);
+ break;
+ default:
+ sprintf(errorStr, "Cannot do number: %d\n", number);
+ return 1;
+ }
+
+ switch (mtype)
+ {
+ case DIM:
+ notes[index] = note_offset(notes[index], -1);
+ break;
+ case MAJ:
+ // It is a major, so not optional
+ optional[index] = 0;
+ case AUG :
+ notes[index] = note_offset(notes[index], 1);
+ break;
+ case NORMAL:
+ case ADD:
+ break;
+ default:
+ break;
+ }
+
+ index++;
+ }
+ return 0;
+}
+
+//
+// Print out chord by name
+//
+void
+Chord::print(char *cname)
+{
+ printf("Notes for chord '%s': ", cname);
+ for (int i = 0; i < 12; i++)
+ {
+ if (notes[i] != -1)
+ printf("%s ", strNotes[notes[i]]);
+ }
+ printf("\n\n");
+}
+
+//
+// Are all the notes in this chord covered by the notes in the
+// noteArray, it is not necessary to cover the notes in the optional
+// array of the chord
+//
+int
+Chord::covered(int *noteArray)
+{
+ // noteArray is an array of notes this chord has, it is NUM_STRINGS notes
+ // long (like a guitar fretboard dude...unused notes may be set
+ // to -1 (which wont compare since -1 is tossed...
+
+ for (int i = 0; i < 12; i++)
+ {
+ if (notes[i] != -1)
+ {
+ int gotIt = 0;
+ for (int j = 0; j < NUM_STRINGS; j++)
+ {
+ if (noteArray[j] == notes[i])
+ {
+ gotIt = 1;
+ break;
+ }
+ }
+ // If it was not found, and it is NOT optional, then it is
+ // not covered
+ if ((gotIt == 0) && (optional[i] == 0))
+ return 0;
+ }
+ }
+ return 1;
+}
+
+
+//
+// Is the given note in the chord
+//
+int
+Chord::inChord(int note)
+{
+ for (int i = 0; i < 12; i++)
+ {
+ // Check if we are off the end of the notes set
+ if (notes[i] == -1)
+ return 0;
+ // Check if the note was found
+ if (note == notes[i])
+ return 1;
+ }
+ // Did not find out, return 0
+ return 0;
+}
+
+//
+// Given a chord string, pick off the root (either C or C# or Cb)...and
+// return that integer value (A = 0)
+//
+int
+Chord::get_base(char *s)
+{
+
+ static int halfsteps[] = { 0, 2, 3, 5, 7, 8, 10 };
+
+ if ((*s < 'A') || (*s > 'G'))
+ return 0;
+
+ if (s[1] == '#')
+ return ( note_offset(halfsteps[s[0] - 'A'], 1));
+ else if (s[1] == 'B')
+ return ( note_offset(halfsteps[s[0] - 'A'], -1));
+ else
+ return ( halfsteps[s[0] - 'A']);
+}
+
+
+//
+// Print out the current fretboard
+//
+void
+Fretboard::print()
+{
+ printf("SCORE: %3d ", score[SCORE_TOTAL]);
+ printf(
+ " SPN: %2d TSPN: %2d OS: %2d ROOT: %2d LOW %2d ADJ %2d",
+ score[SCORE_SPAN], score[SCORE_TOTALSPAN], score[SCORE_OPENSTRINGS],
+ score[SCORE_FIRSTROOT], score[SCORE_LOWONNECK],
+ score[SCORE_ADJNOTES]);
+
+ printf(" FB: ");
+ for (int i = 0; i < NUM_STRINGS; i++)
+ {
+ if (fretboard[i] != -1)
+ printf(" %2d", fretboard[i]);
+ else
+ printf(" X");
+ }
+
+ printf(" NT: ");
+ for (int i = 0; i < NUM_STRINGS; i++)
+ if (notes[i] != -1)
+ printf(" %s", strNotes[notes[i]]);
+ else
+ printf(" X ");
+ printf("\n");
+}
+
+//
+// Construct a fretboard -- reset to the openStrings, clear the stack
+// and reset all the bestScores to -1
+//
+Fretboard::Fretboard()
+{
+ sDepth = 0;
+ score[0] = 0;
+ for (int i = 0; i < NUM_STRINGS; i++)
+ {
+ notes[i] = openStrings[i];
+ fretboard[i] = 0;
+ }
+ for (int i = 0; i < CHORDSTACK; i++)
+ {
+ bestScores[i][0] = -1;
+ }
+}
+
+//
+// Get the span of this chord, don't count open strings
+//
+int
+Fretboard::getSpan()
+{
+ int min = 100, max = 0;
+ for (int i = 0; i < NUM_STRINGS; i++)
+ {
+ // Dont count X strings or open strings
+ if (fretboard[i] <= 0)
+ continue;
+ if (fretboard[i] > max) max = fretboard[i];
+ if (fretboard[i] < min) min = fretboard[i];
+ }
+ if (min == 100)
+ // All open strings, took awhile to catch this bug
+ return 0;
+ else
+ return (max - min);
+}
+
+//
+// Get the span of this chord, DO count open strings
+//
+int
+Fretboard::getTotalSpan()
+{
+ int min = 100, max = 0;
+ for (int i = 0; i < NUM_STRINGS; i++)
+ {
+ // Dont count X strings
+ if (fretboard[i] < 0)
+ continue;
+ if (fretboard[i] > max) max = fretboard[i];
+ if (fretboard[i] < min) min = fretboard[i];
+ }
+ if (min == -1)
+ min = 0;
+ return (max - min);
+}
+
+//
+// Add this chord to the best (if there is room in the stack)
+//
+void
+Fretboard::addToBest()
+{
+ // CHORDSTACK is the sDepth of keepers...
+#ifdef DEBUG
+ printf("ATB: ");
+ this->print();
+#endif
+
+ int i;
+
+ // NOTE: at the start, bestScores is full of -1's, so any reall
+ // real score will be better (worst score is 0)
+ for (i = 0; i < sDepth; i++)
+ {
+ if (score[0] > bestScores[i][0])
+ break;
+ }
+
+ // If score was not better than any in the stack just return
+ if (i >= CHORDSTACK)
+ return ;
+ // MOve down old guys to make room for the new guy
+ for (int j = CHORDSTACK - 1; j >= i; j--)
+ {
+ for (int q = 0; q < NUM_STRINGS; q++)
+ {
+ bestFrets[j][q] = bestFrets[j - 1][q];
+ bestNotes[j][q] = bestNotes[j - 1][q];
+ }
+ for (int q = 0; q < NUMSCORETYPES; q++)
+ bestScores[j][q] = bestScores[j - 1][q];
+ }
+
+ for (int q = 0; q < NUM_STRINGS; q++)
+ {
+ bestFrets[i][q] = fretboard[q];
+ bestNotes[i][q] = notes[q];
+ }
+ for (int q = 0; q < NUMSCORETYPES; q++)
+ bestScores[i][q] = score[q];
+
+ sDepth++;
+ if (sDepth > CHORDSTACK)
+ sDepth--;
+}
+
+//
+// Print out the stack to the screen
+//
+void
+Fretboard::printStack()
+{
+ static char *strNotes[] = { "A ", "A#", "B ", "C ", "C#", "D ",
+ "D#", "E ", "F ", "F#", "G ", "G#" };
+
+
+ for (int f = 0; f < sDepth; f++)
+ {
+ printf("\n\n ");
+
+ if(lefty) {
+ for (int i = NUM_STRINGS - 1; i >= 0; i--)
+ if (bestNotes[f][i] != -1)
+ printf(" %s", strNotes[bestNotes[f][i]]);
+ else
+ printf(" X ");
+ }
+ else {
+ for (int i = 0; i < NUM_STRINGS; i++)
+ if (bestNotes[f][i] != -1)
+ printf(" %s", strNotes[bestNotes[f][i]]);
+ else
+ printf(" X ");
+ }
+ printf("\n");
+ if(lefty) {
+ for (int i = NUM_STRINGS - 1; i >= 0; i--)
+ if (bestFrets[f][i] != -1)
+ printf(" %2d", bestFrets[f][i]);
+ else
+ printf(" X");
+
+ }
+ else {
+ for (int i = 0; i < NUM_STRINGS; i++)
+
+ if (bestFrets[f][i] != -1)
+ printf(" %2d", bestFrets[f][i]);
+ else
+ printf(" X");
+ }
+
+ printf("\n\n");
+
+
+ int highest = 0;
+
+ for(int i = 0; i < NUM_STRINGS; i++)
+ if(bestFrets[f][i] > highest)
+ highest = bestFrets[f][i];
+
+ printf("\n");
+
+ for(int i = 0; i <= (highest + 1); i ++) {
+ if(lefty) {
+ for (int x = NUM_STRINGS-1; x >= 0; x--) {
+ if(i == 0) {
+ if(bestFrets[f][x] == -1)
+ printf(" X");
+ else
+ if(bestFrets[f][x] == 0)
+ printf(" 0");
+ else
+ printf(" ");
+ }
+ else {
+ if(bestFrets[f][x] == i)
+ printf(" *");
+ else
+ printf(" |");
+ }
+ }
+ }
+ else {
+
+ for (int x = 0; x < NUM_STRINGS; x++) {
+ if(i == 0) {
+ if(bestFrets[f][x] == -1)
+ printf(" X");
+ else
+ if(bestFrets[f][x] == 0)
+ printf(" 0");
+ else
+ printf(" ");
+ }
+ else {
+ if(bestFrets[f][x] == i)
+ printf(" *");
+ else
+ printf(" |");
+ }
+ }
+ }
+ printf("\n ---------------- %2d\n",i);
+
+
+
+
+ }
+ }
+}
+
+
+//
+// Get the score for this chord
+//
+void
+Fretboard::getScore(Chord &chord)
+{
+
+ // First, points for small span (excluding opens)
+ score[SCORE_SPAN] = (MAXSPAN - getSpan()) * SPAN_SCORE;
+
+ // Then, points for small total span
+ score[SCORE_TOTALSPAN] = (15 - getTotalSpan()) * 3;
+
+ score[SCORE_OPENSTRINGS] = 0;
+ // Points for open strings
+ for (int i = 0; i < NUM_STRINGS; i++)
+ {
+ if (fretboard[i] == 0)
+ {
+ score[SCORE_OPENSTRINGS] += OPENS_SCORE;
+ }
+ }
+
+ // Points for first string being the root ...
+ score[SCORE_FIRSTROOT] = 0;
+ int i;
+ for (i = 0; (fretboard[i] == -1) && (i < NUM_STRINGS) ; i++)
+ ;
+ if (notes[i] == chord.getRoot())
+ {
+ score[SCORE_FIRSTROOT] = ROOT_SCORE;
+ }
+
+ // Points for being low on the neck...
+ int sum = 0, cnt = 0;
+ for (i = 0; i < NUM_STRINGS; i++)
+ {
+ // Don't count X strings or open strings
+ if (fretboard[i] > 0)
+ {
+ sum += fretboard[i];
+ cnt++;
+ }
+ }
+ if (cnt)
+ score[SCORE_LOWONNECK] = (int)(15 - ((double) sum / (double) cnt))
+ * POSITION_SCORE;
+ else
+ score[SCORE_LOWONNECK] = 15 * POSITION_SCORE;
+
+
+ int adjNotes = 0;
+ for (i = 0; i < 5; i++)
+ {
+ if ((notes[i] != -1) && (notes[i] == notes[i + 1]))
+ adjNotes++;
+ }
+ score[SCORE_ADJNOTES] = (ADJ_SCORE * (5 - adjNotes));
+
+ // FInally, total up the score
+ score[SCORE_TOTAL] = 0;
+ for (i = 1; i < NUMSCORETYPES; i++)
+ score[SCORE_TOTAL] += score[i];
+}
+
+
+//
+// Iterate over all fretboard config's for this chord, call addToBest
+// with any good ones (ie: < span, etc etc...)
+//
+void
+Fretboard::iterate(Chord &chord)
+{
+ int string = 0;
+
+ // Start notes setup, increment up the neck for each string until
+ // you find a note that is in this chord (may be an open note)
+ for (int i = 0; i < NUM_STRINGS; i++)
+ {
+ while (! chord.inChord(notes[i]))
+ {
+ fretboard[i]++;
+ notes[i] = ( notes[i] + 1 ) % 12;
+ }
+ }
+
+ // Back up the first note one...so the loop will work
+ fretboard[0] = -1;
+
+ // While we are still on the fretboard!
+ while (string < NUM_STRINGS)
+ {
+
+ // increment the current string
+ fretboard[string]++;
+ if (fretboard[string] == 0)
+ notes[string] = openStrings[string];
+ else
+ notes[string] = ( notes[string] + 1 ) % 12;
+
+ while (! chord.inChord(notes[string]))
+ {
+ fretboard[string]++;
+ notes[string] = ( notes[string] + 1 ) % 12;
+ }
+
+ if (fretboard[string] > 15)
+ {
+
+ // Before turning over the 3rd string from the bass, try
+ // to make a chord with the bass string, and 2nd from
+ // bass string X'ed out...(ie: set to -1)
+ if (string == 0)
+ {
+
+ notes[0] = fretboard[0] = -1;
+ int span = getSpan();
+ if ((span < MAXSPAN) && chord.covered(notes))
+ {
+ getScore(chord);
+ addToBest();
+ }
+ }
+
+ if (string == 1)
+ {
+ int store = notes[0];
+ int fstore = fretboard[0];
+ notes[1] = fretboard[1] = -1;
+ notes[0] = fretboard[0] = -1;
+ int span = getSpan();
+ if ((span < MAXSPAN) && chord.covered(notes))
+ {
+ getScore(chord);
+ addToBest();
+ }
+ // Restore the notes you X'ed out
+ notes[0] = store;
+ fretboard[0] = fstore;
+ }
+
+ fretboard[string] = 0;
+ notes[string] = openStrings[string];
+ while (! chord.inChord(notes[string]))
+ {
+ fretboard[string]++;
+ notes[string] = chord.note_offset(notes[string], 1);
+ }
+ string++;
+ continue;
+ }
+
+#ifdef DEBUG
+ printf("TRY: "); this->print();
+#endif
+
+ string = 0;
+ int span = getSpan();
+ if (span >= MAXSPAN)
+ {
+#ifdef DEBUG
+ printf("Rejected for span\n");
+#endif
+ continue;
+ }
+ if (!chord.covered(notes))
+ {
+#ifdef DEBUG
+ printf("Rejected for coverage\n");
+#endif
+ continue;
+ }
+
+ getScore(chord);
+
+ addToBest();
+ }
+}
+
+//
+// uh, main
+//
+int main(int argc, char **argv)
+{
+ char buf[256], buf2[256];
+
+ if(argc > 1)
+ {
+ strcpy(buf, argv[1]);
+ if(argc > 3) {
+ if(! strcmp(argv[3],"lefty")) {
+ lefty = 1;
+
+ }
+
+
+ }
+
+ if(argc > 2) {
+ if(! strcmp(argv[2],"dadgad")) {
+ openStrings[0] = 5;
+ openStrings[1] = 0;
+ openStrings[2] = 5;
+ openStrings[3] = 10;
+ openStrings[4] = 0;
+ openStrings[5] = 5;
+ }
+ if(! strcmp(argv[2],"openg")) {
+ openStrings[0] = 5;
+ openStrings[1] = 10;
+ openStrings[2] = 5;
+ openStrings[3] = 10;
+ openStrings[4] = 2;
+ openStrings[5] = 5;
+
+ }
+ if(! strcmp(argv[2],"opene")) {
+ openStrings[0] = 7;
+ openStrings[1] = 2;
+ openStrings[2] = 7;
+ openStrings[3] = 11;
+ openStrings[4] = 2;
+ openStrings[5] = 7;
+
+ }
+ if(! strcmp(argv[2],"lefty")) {
+ lefty = 1;
+
+ }
+ }
+
+
+ // Allocate it for DOS/WINDOWS, to avoid stack overflow (weak)
+ Fretboard *fb = new Fretboard; ;
+ Chord chord;
+
+
+ // findChord upppercases the input string, so save a copy
+ strcpy(buf2, buf);
+
+ if (chord.findChord(buf))
+ {
+ chord.printError();
+
+ }
+ else {
+ chord.print(buf2);
+ fb->iterate(chord);
+
+ fb->printStack();
+ }
+ delete fb;
+ }
+ return 0;
+}
+
diff --git a/sources/addons/chords/chords.apd b/sources/addons/chords/chords.apd
new file mode 100644
index 00000000..2ab5b99d
--- /dev/null
+++ b/sources/addons/chords/chords.apd
@@ -0,0 +1,3 @@
+url: $baseurl/chords
+name: Guitar Chords
+photo: $baseurl/addon/chords/chords.png
diff --git a/sources/addons/chords/chords.php b/sources/addons/chords/chords.php
new file mode 100755
index 00000000..93bdbd94
--- /dev/null
+++ b/sources/addons/chords/chords.php
@@ -0,0 +1,129 @@
+
+ */
+
+
+function chords_load() {
+ register_hook('app_menu', 'addon/chords/chords.php', 'chords_app_menu');
+}
+
+function chords_unload() {
+ unregister_hook('app_menu', 'addon/chords/chords.php', 'chords_app_menu');
+
+}
+
+function chords_app_menu($a,&$b) {
+ $b['app_menu'][] = '';
+}
+
+
+function chords_module() {}
+
+
+function chords_content($a) {
+
+
+$o .= 'Guitar Chords ';
+$o .= 'The complete online guitar chord dictionary ';
+$args = '';
+$l = '';
+
+if($_SERVER['REQUEST_METHOD'] == 'POST') {
+ if(isset($_POST['chord']) && strlen($_POST['chord']))
+ $args .= escapeshellarg(ucfirst(trim($_POST['chord'])));
+ if((strlen($args)) && (isset($_POST['tuning'])) && (strlen($_POST['tuning'])))
+ $args .= ' '.escapeshellarg($_POST['tuning']);
+ if((strlen($args)) && (isset($_POST['lefty'])))
+ $args .= ' lefty';
+}
+
+if((! strlen($args)) && (! stristr(basename($_SERVER['QUERY_STRING']),'chords')) && strlen(basename($_SERVER['QUERY_STRING'])))
+ $args = escapeshellarg(ucfirst(basename($_SERVER['QUERY_STRING'])));
+
+$tunings = array("","openg", "opene", "dadgad");
+$tnames = array("Em11 [Standard] (EADGBE)",
+ "G/D [Drop D] (DGDGBD)","Open E (EBEG#BE)","Dsus4 (DADGAD)");
+$t = ((isset($_POST['tuning'])) ? $_POST['tuning'] : '');
+if(isset($_POST['lefty']) && $_POST['lefty'] == '1')
+ $l = 'checked="checked"';
+
+ $ch = ((isset($_POST['chord'])) ? $_POST['chord'] : '');
+$o .= <<< EOT
+
+
+
+EOT;
+
+if(strlen($args)) {
+ $o .= '';
+ $o .= shell_exec("addon/chords/chord ".$args);
+ $o .= ' ';
+}
+else {
+
+$o .= <<< EOT
+
+
+This is a fairly comprehensive and complete guitar chord dictionary which will list most of the available ways to play a certain chord, starting from the base of the fingerboard up to a few frets beyond the twelfth fret (beyond which everything repeats). A couple of non-standard tunings are provided for the benefit of slide players, etc.
+
+
+Chord names start with a root note (A-G) and may include sharps (#) and flats (b). This software will parse most of the standard naming conventions such as maj, min, dim, sus(2 or 4), aug, with optional repeating elements.
+
+
+Valid examples include A, A7, Am7, Amaj7, Amaj9, Ammaj7, Aadd4, Asus2Add4, E7b13b11 ...
+
+Quick Reference:
+
+EOT;
+
+$keys = array('A','Bb','B', 'C','Db','D','Eb','E','F','Gb','G','Ab');
+$o .= '';
+$o .= "";
+foreach($keys as $k)
+ $o .= " $k ";
+$o .= " ";
+foreach($keys as $k)
+ $o .= " {$k}m ";
+$o .= " ";
+foreach($keys as $k)
+ $o .= " {$k}7 ";
+$o .= " ";
+$o .= "
";
+
+}
+
+return $o;
+
+}
+
+
+
+
+
+
+
+
+
+
diff --git a/sources/addons/chords/chords.png b/sources/addons/chords/chords.png
new file mode 100644
index 0000000000000000000000000000000000000000..9427943fc0980ea32facb4454e9d73af31cc5dd0
GIT binary patch
literal 11413
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
+ */
+
+
+function custom_home_load() {
+ register_hook('home_mod_content', 'addon/custom_home/custom_home.php', 'custom_home_home');
+ logger("loaded custom_home");
+}
+
+function custom_home_unload() {
+ unregister_hook('home_mod_content', 'addon/custom_home/custom_home.php', 'custom_home_home');
+ unregister_hook('home_content', 'addon/custom_home/custom_home.php', 'custom_home_home');
+ logger("removed custom_home");
+}
+
+function custom_home_home(&$a, &$o){
+
+ $x = get_config('system','custom_home');
+ if($x) {
+ if ($x == "random") {
+ $rand = db_getfunc('rand');
+ $r = q("select channel_address from channel where channel_r_stream = 1 and channel_address != 'sys' order by $rand limit 1");
+ $x = z_root() . '/channel/' . $r[0]['channel_address'];
+ }
+ else {
+ $x = z_root() . '/' . $x;
+ }
+
+ goaway(zid($x));
+ }
+
+//If nothing is set
+ return $o;
+}
+
diff --git a/sources/addons/dfedfix/dfedfix.php b/sources/addons/dfedfix/dfedfix.php
new file mode 100644
index 00000000..af7b3837
--- /dev/null
+++ b/sources/addons/dfedfix/dfedfix.php
@@ -0,0 +1,30 @@
+','
+' . z_root() . '/channel/' . $b['user']['channel_address'] . ' ',$x);
+ $x = str_replace('.AQAB" />','.AQAB "/>
+ ',$x);
+ $b['xml'] = $x;
+
+}
+
\ No newline at end of file
diff --git a/sources/addons/diaspora/diaspora.php b/sources/addons/diaspora/diaspora.php
new file mode 100755
index 00000000..b2e21013
--- /dev/null
+++ b/sources/addons/diaspora/diaspora.php
@@ -0,0 +1,3397 @@
+',''),
+ array('',' '),
+ $msg['message']);
+
+
+ $parsed_xml = parse_xml_string($msg['message'],false);
+
+ $xmlbase = $parsed_xml->post;
+
+// logger('diaspora_dispatch: ' . print_r($xmlbase,true), LOGGER_DATA);
+
+
+ if($xmlbase->request) {
+ $ret = diaspora_request($importer,$xmlbase->request);
+ }
+ elseif($xmlbase->status_message) {
+ $ret = diaspora_post($importer,$xmlbase->status_message,$msg);
+ }
+ elseif($xmlbase->profile) {
+ $ret = diaspora_profile($importer,$xmlbase->profile,$msg);
+ }
+ elseif($xmlbase->comment) {
+ $ret = diaspora_comment($importer,$xmlbase->comment,$msg);
+ }
+ elseif($xmlbase->like) {
+ $ret = diaspora_like($importer,$xmlbase->like,$msg);
+ }
+ elseif($xmlbase->asphoto) {
+ $ret = diaspora_asphoto($importer,$xmlbase->asphoto,$msg);
+ }
+ elseif($xmlbase->reshare) {
+ $ret = diaspora_reshare($importer,$xmlbase->reshare,$msg);
+ }
+ elseif($xmlbase->retraction) {
+ $ret = diaspora_retraction($importer,$xmlbase->retraction,$msg);
+ }
+ elseif($xmlbase->signed_retraction) {
+ $ret = diaspora_signed_retraction($importer,$xmlbase->signed_retraction,$msg);
+ }
+ elseif($xmlbase->relayable_retraction) {
+ $ret = diaspora_signed_retraction($importer,$xmlbase->relayable_retraction,$msg);
+ }
+ elseif($xmlbase->photo) {
+ $ret = diaspora_photo($importer,$xmlbase->photo,$msg);
+ }
+ elseif($xmlbase->conversation) {
+ $ret = diaspora_conversation($importer,$xmlbase->conversation,$msg);
+ }
+ elseif($xmlbase->message) {
+ $ret = diaspora_message($importer,$xmlbase->message,$msg);
+ }
+ else {
+ logger('diaspora_dispatch: unknown message type: ' . print_r($xmlbase,true));
+ }
+ return $ret;
+}
+
+
+function diaspora_is_blacklisted($s) {
+
+ if(! check_siteallowed($s)) {
+ logger('blacklisted site: ' . $s);
+ return true;
+ }
+
+ return false;
+}
+
+function diaspora_process_outbound(&$a, &$arr) {
+
+/*
+
+ We are passed the following array from the notifier, providing everything we need to make delivery decisions.
+
+ $arr = array(
+ 'channel' => $channel,
+ 'env_recips' => $env_recips,
+ 'recipients' => $recipients,
+ 'item' => $item,
+ 'target_item' => $target_item,
+ 'hub' => $hub,
+ 'top_level_post' => $top_level_post,
+ 'private' => $private,
+ 'followup' => $followup,
+ 'relay_to_owner' => $relay_to_owner,
+ 'uplink' => $uplink,
+ 'cmd' => $cmd,
+ 'expire' => $expire,
+ 'mail' => $mail,
+ 'location' => $location,
+ 'fsuggest' => $fsuggest,
+ 'normal_mode' => $normal_mode,
+ 'packet_type' => $packet_type,
+ 'walltowall' => $walltowall,
+ 'queued' => pass these queued items (outq_hash) back to notifier.php for delivery
+ );
+*/
+
+
+ // allow this to be set per message
+
+ if(strpos($arr['target_item']['postopts'],'nodspr') !== false)
+ return;
+
+ $allowed = get_pconfig($arr['channel']['channel_id'],'system','diaspora_allowed');
+
+ if(! intval($allowed)) {
+ logger('mod-diaspora: disallowed for channel ' . $arr['channel']['channel_name']);
+ return;
+ }
+
+
+ if($arr['location'])
+ return;
+
+
+ $target_item = $arr['target_item'];
+
+ if($target_item && array_key_exists('item_obscured',$target_item) && intval($target_item['item_obscured'])) {
+ $key = get_config('system','prvkey');
+ if($target_item['title'])
+ $target_item['title'] = crypto_unencapsulate(json_decode($target_item['title'],true),$key);
+ if($target_item['body'])
+ $target_item['body'] = crypto_unencapsulate(json_decode($target_item['body'],true),$key);
+ }
+
+
+
+ if($arr['env_recips']) {
+ $hashes = array();
+
+ // re-explode the recipients, but only for this hub/pod
+
+ foreach($arr['env_recips'] as $recip)
+ $hashes[] = "'" . $recip['hash'] . "'";
+
+ $r = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_url = '%s'
+ and xchan_hash in (" . implode(',', $hashes) . ") and xchan_network in ('diaspora', 'friendica-over-diaspora') ",
+ dbesc($arr['hub']['hubloc_url'])
+ );
+
+ if(! $r) {
+ logger('diaspora_process_outbound: no recipients');
+ return;
+ }
+
+ foreach($r as $contact) {
+
+ if($arr['mail']) {
+ $qi = diaspora_send_mail($arr['item'],$arr['channel'],$contact);
+ if($qi)
+ $arr['queued'][] = $qi;
+ continue;
+ }
+
+ if(! $arr['normal_mode'])
+ continue;
+
+ // special handling for followup to public post
+ // all other public posts processed as public batches further below
+
+ if((! $arr['private']) && ($arr['followup'])) {
+ $qi = diaspora_send_followup($target_item,$arr['channel'],$contact, true);
+ if($qi)
+ $arr['queued'][] = $qi;
+ continue;
+ }
+
+ if(! $contact['xchan_pubkey'])
+ continue;
+
+ if(intval($target_item['item_deleted'])
+ && (($target_item['mid'] === $target_item['parent_mid']) || $arr['followup'])) {
+ // send both top-level retractions and relayable retractions for owner to relay
+ $qi = diaspora_send_retraction($target_item,$arr['channel'],$contact);
+ if($qi)
+ $arr['queued'][] = $qi;
+ continue;
+ }
+ elseif($arr['followup']) {
+ // send comments and likes to owner to relay
+ $qi = diaspora_send_followup($target_item,$arr['channel'],$contact);
+ if($qi)
+ $arr['queued'][] = $qi;
+ continue;
+ }
+
+ elseif($target_item['mid'] !== $target_item['parent_mid']) {
+ // we are the relay - send comments, likes and relayable_retractions
+ // (of comments and likes) to our conversants
+ $qi = diaspora_send_relay($target_item,$arr['channel'],$contact);
+ if($qi)
+ $arr['queued'][] = $qi;
+ continue;
+ }
+ elseif($arr['top_level_post']) {
+ $qi = diaspora_send_status($target_item,$arr['channel'],$contact);
+ if($qi)
+ $arr['queued'][] = $qi;
+ continue;
+ }
+ }
+ }
+ else {
+ // public message
+
+ $contact = $arr['hub'];
+
+ if(intval($target_item['item_deleted'])
+ && ($target_item['mid'] === $target_item['parent_mid'])) {
+ // top-level retraction
+ logger('delivery: diaspora retract: ' . $loc);
+ $qi = diaspora_send_retraction($target_item,$arr['channel'],$contact,true);
+ if($qi)
+ $arr['queued'][] = $qi;
+ return;
+ }
+ elseif($target_item['mid'] !== $target_item['parent_mid']) {
+ // we are the relay - send comments, likes and relayable_retractions to our conversants
+ logger('delivery: diaspora relay: ' . $loc);
+ $qi = diaspora_send_relay($target_item,$arr['channel'],$contact,true);
+ if($qi)
+ $arr['queued'][] = $qi;
+ return;
+ }
+ elseif($arr['top_level_post']) {
+ logger('delivery: diaspora status: ' . $loc);
+ $qi = diaspora_send_status($target_item,$arr['channel'],$contact,true);
+ if($qi)
+ $arr['queued'][] = $qi;
+ return;
+ }
+
+ }
+
+
+}
+
+
+function diaspora_handle_from_contact($contact_hash) {
+
+ logger("diaspora_handle_from_contact: contact id is " . $contact_hash, LOGGER_DEBUG);
+
+ $r = q("SELECT xchan_addr from xchan where xchan_hash = '%s' limit 1",
+ dbesc($contact_hash)
+ );
+ if($r) {
+ return $r[0]['xchan_addr'];
+ }
+ return false;
+}
+
+function diaspora_get_contact_by_handle($uid,$handle) {
+
+ if(diaspora_is_blacklisted($handle))
+ return false;
+ require_once('include/identity.php');
+
+ $sys = get_sys_channel();
+ if(($sys) && ($sys['channel_id'] == $uid)) {
+ $r = q("SELECT * FROM xchan where xchan_addr = '%s' limit 1",
+ dbesc($handle)
+ );
+ }
+ else {
+ $r = q("SELECT * FROM abook left join xchan on xchan_hash = abook_xchan where xchan_addr = '%s' and abook_channel = %d limit 1",
+ dbesc($handle),
+ intval($uid)
+ );
+ }
+
+ return (($r) ? $r[0] : false);
+}
+
+function find_diaspora_person_by_handle($handle) {
+
+ $person = false;
+ $refresh = false;
+
+ if(diaspora_is_blacklisted($handle))
+ return false;
+
+ $r = q("select * from xchan where xchan_addr = '%s' limit 1",
+ dbesc($handle)
+ );
+ if($r) {
+ $person = $r[0];
+ logger('find_diaspora_person_by handle: in cache ' . print_r($r,true), LOGGER_DATA);
+ if($person['xchan_name_date'] < datetime_convert('UTC','UTC', 'now - 1 month')) {
+ logger('Updating Diaspora cached record for ' . $handle);
+ $refresh = true;
+ }
+ }
+
+ if((! $person) || ($refresh)) {
+
+ // try webfinger. Make sure to distinguish between diaspora,
+ // hubzilla w/diaspora protocol and friendica w/diaspora protocol.
+
+ $result = discover_by_webbie($handle);
+ if($result) {
+ $r = q("select * from xchan where xchan_addr = '%s' limit 1",
+ dbesc(str_replace('acct:','',$handle))
+ );
+ if($r) {
+ $person = $r[0];
+ logger('find_diaspora_person_by handle: discovered ' . print_r($r,true), LOGGER_DATA);
+ }
+ }
+ }
+
+ return $person;
+}
+
+
+function get_diaspora_key($handle) {
+ logger('Fetching diaspora key for: ' . $handle, LOGGER_DEBUG);
+ $r = find_diaspora_person_by_handle($handle);
+ return(($r) ? $r['xchan_pubkey'] : '');
+}
+
+
+function diaspora_pubmsg_build($msg,$channel,$contact,$prvkey,$pubkey) {
+
+ $a = get_app();
+
+ logger('diaspora_pubmsg_build: ' . $msg, LOGGER_DATA);
+
+ $handle = $channel['channel_address'] . '@' . get_app()->get_hostname();
+
+
+ $b64url_data = base64url_encode($msg,false);
+
+ $data = str_replace(array("\n","\r"," ","\t"),array('','','',''),$b64url_data);
+
+ $type = 'application/xml';
+ $encoding = 'base64url';
+ $alg = 'RSA-SHA256';
+
+ $signable_data = $data . '.' . base64url_encode($type,false) . '.'
+ . base64url_encode($encoding,false) . '.' . base64url_encode($alg,false) ;
+
+ $signature = rsa_sign($signable_data,$prvkey);
+ $sig = base64url_encode($signature,false);
+
+$magic_env = <<< EOT
+
+
+
+
+ base64url
+ RSA-SHA256
+ $data
+ $sig
+
+
+EOT;
+
+ logger('diaspora_pubmsg_build: magic_env: ' . $magic_env, LOGGER_DATA);
+ return $magic_env;
+
+}
+
+
+
+
+function diaspora_msg_build($msg,$channel,$contact,$prvkey,$pubkey,$public = false) {
+ $a = get_app();
+
+ if($public)
+ return diaspora_pubmsg_build($msg,$channel,$contact,$prvkey,$pubkey);
+
+ logger('diaspora_msg_build: ' . $msg, LOGGER_DATA);
+
+ // without a public key nothing will work
+
+ if(! $pubkey) {
+ logger('diaspora_msg_build: pubkey missing: contact id: ' . $contact['abook_id']);
+ return '';
+ }
+
+ $inner_aes_key = random_string(32);
+ $b_inner_aes_key = base64_encode($inner_aes_key);
+ $inner_iv = random_string(16);
+ $b_inner_iv = base64_encode($inner_iv);
+
+ $outer_aes_key = random_string(32);
+ $b_outer_aes_key = base64_encode($outer_aes_key);
+ $outer_iv = random_string(16);
+ $b_outer_iv = base64_encode($outer_iv);
+
+ $handle = $channel['channel_address'] . '@' . get_app()->get_hostname();
+
+ $padded_data = pkcs5_pad($msg,16);
+ $inner_encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $inner_aes_key, $padded_data, MCRYPT_MODE_CBC, $inner_iv);
+
+ $b64_data = base64_encode($inner_encrypted);
+
+
+ $b64url_data = base64url_encode($b64_data,false);
+ $data = str_replace(array("\n","\r"," ","\t"),array('','','',''),$b64url_data);
+
+ $type = 'application/xml';
+ $encoding = 'base64url';
+ $alg = 'RSA-SHA256';
+
+ $signable_data = $data . '.' . base64url_encode($type,false) . '.'
+ . base64url_encode($encoding,false) . '.' . base64url_encode($alg,false) ;
+
+ logger('diaspora_msg_build: signable_data: ' . $signable_data, LOGGER_DATA);
+
+ $signature = rsa_sign($signable_data,$prvkey);
+ $sig = base64url_encode($signature,false);
+
+$decrypted_header = <<< EOT
+
+ $b_inner_iv
+ $b_inner_aes_key
+ $handle
+
+EOT;
+
+ $decrypted_header = pkcs5_pad($decrypted_header,16);
+
+ $ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $outer_aes_key, $decrypted_header, MCRYPT_MODE_CBC, $outer_iv);
+
+ $outer_json = json_encode(array('iv' => $b_outer_iv,'key' => $b_outer_aes_key));
+
+ $encrypted_outer_key_bundle = '';
+ openssl_public_encrypt($outer_json,$encrypted_outer_key_bundle,$pubkey);
+
+ $b64_encrypted_outer_key_bundle = base64_encode($encrypted_outer_key_bundle);
+
+ logger('outer_bundle: ' . $b64_encrypted_outer_key_bundle . ' key: ' . $pubkey, LOGGER_DATA);
+
+ $encrypted_header_json_object = json_encode(array('aes_key' => base64_encode($encrypted_outer_key_bundle),
+ 'ciphertext' => base64_encode($ciphertext)));
+ $cipher_json = base64_encode($encrypted_header_json_object);
+
+ $encrypted_header = '' . $cipher_json . ' ';
+
+$magic_env = <<< EOT
+
+
+ $encrypted_header
+
+ base64url
+ RSA-SHA256
+ $data
+ $sig
+
+
+EOT;
+
+ logger('diaspora_msg_build: magic_env: ' . $magic_env, LOGGER_DATA);
+ return $magic_env;
+
+}
+
+/**
+ *
+ * diaspora_decode($importer,$xml)
+ * array $importer -> from user table
+ * string $xml -> urldecoded Diaspora salmon
+ *
+ * Returns array
+ * 'message' -> decoded Diaspora XML message
+ * 'author' -> author diaspora handle
+ * 'key' -> author public key (converted to pkcs#8)
+ *
+ * Author and key are used elsewhere to save a lookup for verifying replies and likes
+ */
+
+
+function diaspora_decode($importer,$xml) {
+
+ $public = false;
+ $basedom = parse_xml_string($xml);
+
+ $children = $basedom->children('https://joindiaspora.com/protocol');
+
+ if($children->header) {
+ $public = true;
+ $author_link = str_replace('acct:','',$children->header->author_id);
+ }
+ else {
+
+ $encrypted_header = json_decode(base64_decode($children->encrypted_header));
+
+ $encrypted_aes_key_bundle = base64_decode($encrypted_header->aes_key);
+ $ciphertext = base64_decode($encrypted_header->ciphertext);
+
+ $outer_key_bundle = '';
+ openssl_private_decrypt($encrypted_aes_key_bundle,$outer_key_bundle,$importer['channel_prvkey']);
+
+ $j_outer_key_bundle = json_decode($outer_key_bundle);
+
+ $outer_iv = base64_decode($j_outer_key_bundle->iv);
+ $outer_key = base64_decode($j_outer_key_bundle->key);
+
+ $decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $outer_key, $ciphertext, MCRYPT_MODE_CBC, $outer_iv);
+
+
+ $decrypted = pkcs5_unpad($decrypted);
+
+ /**
+ * $decrypted now contains something like
+ *
+ *
+ * 8e+G2+ET8l5BPuW0sVTnQw==
+ * UvSMb4puPeB14STkcDWq+4QE302Edu15oaprAQSkLKU=
+
+***** OBSOLETE
+
+ *
+ * Ryan Hughes
+ * acct:galaxor@diaspora.pirateship.org
+ *
+
+***** CURRENT
+
+ * galaxor@diaspora.priateship.org
+
+***** END DIFFS
+
+ *
+ */
+
+ logger('decrypted: ' . $decrypted, LOGGER_DATA);
+ $idom = parse_xml_string($decrypted,false);
+
+ $inner_iv = base64_decode($idom->iv);
+ $inner_aes_key = base64_decode($idom->aes_key);
+
+ $author_link = str_replace('acct:','',$idom->author_id);
+
+ }
+
+ $dom = $basedom->children(NAMESPACE_SALMON_ME);
+
+ // figure out where in the DOM tree our data is hiding
+
+ if($dom->provenance->data)
+ $base = $dom->provenance;
+ elseif($dom->env->data)
+ $base = $dom->env;
+ elseif($dom->data)
+ $base = $dom;
+
+ if(! $base) {
+ logger('mod-diaspora: unable to locate salmon data in xml ');
+ http_status_exit(400);
+ }
+
+
+ // Stash the signature away for now. We have to find their key or it won't be good for anything.
+ $signature = base64url_decode($base->sig);
+
+ // unpack the data
+
+ // strip whitespace so our data element will return to one big base64 blob
+ $data = str_replace(array(" ","\t","\r","\n"),array("","","",""),$base->data);
+
+
+ // stash away some other stuff for later
+
+ $type = $base->data[0]->attributes()->type[0];
+ $keyhash = $base->sig[0]->attributes()->keyhash[0];
+ $encoding = $base->encoding;
+ $alg = $base->alg;
+
+ $signed_data = $data . '.' . base64url_encode($type,false) . '.' . base64url_encode($encoding,false) . '.' . base64url_encode($alg,false);
+
+
+ // decode the data
+ $data = base64url_decode($data);
+
+
+ if($public) {
+ $inner_decrypted = $data;
+ }
+ else {
+
+ // Decode the encrypted blob
+
+ $inner_encrypted = base64_decode($data);
+ $inner_decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $inner_aes_key, $inner_encrypted, MCRYPT_MODE_CBC, $inner_iv);
+ $inner_decrypted = pkcs5_unpad($inner_decrypted);
+ }
+
+ if(! $author_link) {
+ logger('mod-diaspora: Could not retrieve author URI.');
+ http_status_exit(400);
+ }
+
+ // Once we have the author URI, go to the web and try to find their public key
+ // (first this will look it up locally if it is in the fcontact cache)
+ // This will also convert diaspora public key from pkcs#1 to pkcs#8
+
+ logger('mod-diaspora: Fetching key for ' . $author_link );
+ $key = get_diaspora_key($author_link);
+
+ if(! $key) {
+ logger('mod-diaspora: Could not retrieve author key.');
+ http_status_exit(400);
+ }
+
+ $verify = rsa_verify($signed_data,$signature,$key);
+
+ if(! $verify) {
+ logger('mod-diaspora: Message did not verify. Discarding.');
+ http_status_exit(400);
+ }
+
+ logger('mod-diaspora: Message verified.');
+
+ return array('message' => $inner_decrypted, 'author' => $author_link, 'key' => $key);
+
+}
+
+
+/* sender is now sharing with recipient */
+
+function diaspora_request($importer,$xml) {
+
+ $a = get_app();
+
+ $sender_handle = unxmlify($xml->sender_handle);
+ $recipient_handle = unxmlify($xml->recipient_handle);
+
+ if(! $sender_handle || ! $recipient_handle)
+ return;
+
+
+ // Do we already have an abook record?
+
+ $contact = diaspora_get_contact_by_handle($importer['channel_id'],$sender_handle);
+
+ if($contact && $contact['abook_id']) {
+
+ // perhaps we were already sharing with this person. Now they're sharing with us.
+ // That makes us friends. Maybe.
+
+ // Please note some of these permissions such as PERMS_R_PAGES are impossible for Disapora.
+ // They cannot authenticate to our system.
+
+ $newperms = PERMS_R_STREAM|PERMS_R_PROFILE|PERMS_R_PHOTOS|PERMS_R_ABOOK|PERMS_W_STREAM|PERMS_W_COMMENT|PERMS_W_MAIL|PERMS_W_CHAT|PERMS_R_STORAGE|PERMS_R_PAGES;
+
+ $r = q("update abook set abook_their_perms = %d where abook_id = %d and abook_channel = %d",
+ intval($newperms),
+ intval($contact['abook_id']),
+ intval($importer['channel_id'])
+ );
+
+ return;
+ }
+
+ $ret = find_diaspora_person_by_handle($sender_handle);
+
+ if((! $ret) || (! strstr($ret['xchan_network'],'diaspora'))) {
+ logger('diaspora_request: Cannot resolve diaspora handle ' . $sender_handle . ' for ' . $recipient_handle);
+ return;
+ }
+
+
+//FIXME
+/*
+ if(feature_enabled($channel['channel_id'],'premium_channel')) {
+ $myaddr = $importer['channel_address'] . '@' . get_app()->get_hostname();
+ $cnv = random_string();
+ $mid = random_string();
+
+ $msg = t('You have started sharing with a $Projectname premium channel.');
+ $msg .= t('$Projectname premium channels are not available for sharing with Diaspora members. This sharing request has been blocked.') . "\r";
+
+ $msg .= t('Please do not reply to this message, as this channel is not sharing with you and any reply will not be seen by the recipient.') . "\r";
+
+ $created = datetime_convert('UTC','UTC',$item['created'],'Y-m-d H:i:s \U\T\C');
+ $signed_text = $mid . ';' . $cnv . ';' . $msg . ';'
+ . $created . ';' . $myaddr . ';' . $cnv;
+
+ $sig = base64_encode(rsa_sign($signed_text,$importer['channel_prvkey'],'sha256'));
+
+ $conv = array(
+ 'guid' => xmlify($cnv),
+ 'subject' => xmlify(t('Sharing request failed.')),
+ 'created_at' => xmlify($created),
+ 'diaspora_handle' => xmlify($myaddr),
+ 'participant_handles' => xmlify($myaddr . ';' . $sender_handle)
+ );
+
+ $msg = array(
+ 'guid' => xmlify($mid),
+ 'parent_guid' => xmlify($cnv),
+ 'parent_author_signature' => xmlify($sig),
+ 'author_signature' => xmlify($sig),
+ 'text' => xmlify($msg),
+ 'created_at' => xmlify($created),
+ 'diaspora_handle' => xmlify($myaddr),
+ 'conversation_guid' => xmlify($cnv)
+ );
+
+ $conv['messages'] = array($msg);
+ $tpl = get_markup_template('diaspora_conversation.tpl');
+ $xmsg = replace_macros($tpl, array('$conv' => $conv));
+
+ $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($xmsg,$importer,$ret,$importer['channel_prvkey'],$ret['xchan_pubkey'],false)));
+
+ $qi = diaspora_queue($importer,$ret,$slap,false);
+ return $qi;
+ }
+
+*/
+// End FIXME
+
+
+ $role = get_pconfig($channel['channel_id'],'system','permissions_role');
+ if($role) {
+ $x = get_role_perms($role);
+ if($x['perms_auto'])
+ $default_perms = $x['perms_accept'];
+ }
+ if(! $default_perms)
+ $default_perms = intval(get_pconfig($importer['channel_id'],'system','autoperms'));
+
+ $their_perms = PERMS_R_STREAM|PERMS_R_PROFILE|PERMS_R_PHOTOS|PERMS_R_ABOOK|PERMS_W_STREAM|PERMS_W_COMMENT|PERMS_W_MAIL|PERMS_W_CHAT|PERMS_R_STORAGE|PERMS_R_PAGES;
+
+
+ $closeness = get_pconfig($importer['channel_id'],'system','new_abook_closeness');
+ if($closeness === false)
+ $closeness = 80;
+
+
+ $r = q("insert into abook ( abook_account, abook_channel, abook_xchan, abook_my_perms, abook_their_perms, abook_closeness, abook_created, abook_updated, abook_connected, abook_dob, abook_pending) values ( %d, %d, '%s', %d, %d, %d, '%s', '%s', '%s', '%s', %d )",
+ intval($importer['channel_account_id']),
+ intval($importer['channel_id']),
+ dbesc($ret['xchan_hash']),
+ intval($default_perms),
+ intval($their_perms),
+ intval($closeness),
+ dbesc(datetime_convert()),
+ dbesc(datetime_convert()),
+ dbesc(datetime_convert()),
+ dbesc(NULL_DATE),
+ intval(($default_perms) ? 0 : 1)
+ );
+
+
+ if($r) {
+ logger("New Diaspora introduction received for {$importer['channel_name']}");
+
+ $new_connection = q("select * from abook left join xchan on abook_xchan = xchan_hash left join hubloc on hubloc_hash = xchan_hash where abook_channel = %d and abook_xchan = '%s' order by abook_created desc limit 1",
+ intval($importer['channel_id']),
+ dbesc($ret['xchan_hash'])
+ );
+ if($new_connection) {
+ require_once('include/enotify.php');
+ notification(array(
+ 'type' => NOTIFY_INTRO,
+ 'from_xchan' => $ret['xchan_hash'],
+ 'to_xchan' => $importer['channel_hash'],
+ 'link' => z_root() . '/connedit/' . $new_connection[0]['abook_id'],
+ ));
+
+
+ if($default_perms) {
+ // Send back a sharing notification to them
+ $x = diaspora_share($importer,$new_connection[0]);
+ if($x)
+ proc_run('php','include/deliver.php',$x);
+
+ }
+
+ $clone = array();
+ foreach($new_connection[0] as $k => $v) {
+ if(strpos($k,'abook_') === 0) {
+ $clone[$k] = $v;
+ }
+ }
+ unset($clone['abook_id']);
+ unset($clone['abook_account']);
+ unset($clone['abook_channel']);
+
+ build_sync_packet($importer['channel_id'], array('abook' => array($clone)));
+
+ }
+ }
+
+ // find the abook record we just created
+
+ $contact_record = diaspora_get_contact_by_handle($importer['channel_id'],$sender_handle);
+
+ if(! $contact_record) {
+ logger('diaspora_request: unable to locate newly created contact record.');
+ return;
+ }
+
+ /** If there is a default group for this channel, add this member to it */
+
+ if($importer['channel_default_group']) {
+ require_once('include/group.php');
+ $g = group_rec_byhash($importer['channel_id'],$importer['channel_default_group']);
+ if($g)
+ group_add_member($importer['channel_id'],'',$contact_record['xchan_hash'],$g['id']);
+ }
+
+ return;
+}
+
+
+
+function diaspora_post($importer,$xml,$msg) {
+
+ $a = get_app();
+ $guid = notags(unxmlify($xml->guid));
+ $diaspora_handle = notags(unxmlify($xml->diaspora_handle));
+ $app = notags(xmlify($xml->provider_display_name));
+
+
+ if($diaspora_handle != $msg['author']) {
+ logger('diaspora_post: Potential forgery. Message handle is not the same as envelope sender.');
+ return 202;
+ }
+
+ $contact = diaspora_get_contact_by_handle($importer['channel_id'],$diaspora_handle);
+ if(! $contact)
+ return;
+
+
+
+ if(! $app) {
+ if(strstr($contact['xchan_network'],'friendica'))
+ $app = 'Friendica';
+ else
+ $app = 'Diaspora';
+ }
+
+
+ $search_guid = ((strlen($guid) == 64) ? $guid . '%' : $guid);
+
+ $r = q("SELECT id FROM item WHERE uid = %d AND mid like '%s' LIMIT 1",
+ intval($importer['channel_id']),
+ dbesc($search_guid)
+ );
+
+ if($r) {
+ // check dates if post editing is implemented
+ logger('diaspora_post: message exists: ' . $guid);
+ return;
+ }
+
+ $created = unxmlify($xml->created_at);
+ $private = ((unxmlify($xml->public) == 'false') ? 1 : 0);
+
+ $body = diaspora2bb($xml->raw_message);
+
+ if($xml->photo) {
+ $body = '[img]' . $xml->photo->remote_photo_path . $xml->photo->remote_photo_name . '[/img]' . "\n\n" . $body;
+ $body = scale_external_images($body);
+ }
+
+ $maxlen = get_max_import_size();
+
+ if($maxlen && mb_strlen($body) > $maxlen) {
+ $body = mb_substr($body,0,$maxlen,'UTF-8');
+ logger('message length exceeds max_import_size: truncated');
+ }
+
+//WTF? FIXME
+ // Add OEmbed and other information to the body
+// $body = add_page_info_to_body($body, false, true);
+
+ $datarray = array();
+
+
+ // Look for tags and linkify them
+ $results = linkify_tags(get_app(), $body, $importer['channel_id'], true);
+
+ $datarray['term'] = array();
+
+ if($results) {
+ foreach($results as $result) {
+ $success = $result['success'];
+ if($success['replaced']) {
+ $datarray['term'][] = array(
+ 'uid' => $importer['channel_id'],
+ 'type' => $success['termtype'],
+ 'otype' => TERM_OBJ_POST,
+ 'term' => $success['term'],
+ 'url' => $success['url']
+ );
+ }
+ }
+ }
+
+ $cnt = preg_match_all('/@\[url=(.*?)\](.*?)\[\/url\]/ism',$body,$matches,PREG_SET_ORDER);
+ if($cnt) {
+ foreach($matches as $mtch) {
+ $datarray['term'][] = array(
+ 'uid' => $importer['channel_id'],
+ 'type' => TERM_MENTION,
+ 'otype' => TERM_OBJ_POST,
+ 'term' => $mtch[2],
+ 'url' => $mtch[1]
+ );
+ }
+ }
+
+ $cnt = preg_match_all('/@\[zrl=(.*?)\](.*?)\[\/zrl\]/ism',$body,$matches,PREG_SET_ORDER);
+ if($cnt) {
+ foreach($matches as $mtch) {
+ // don't include plustags in the term
+ $term = ((substr($mtch[2],-1,1) === '+') ? substr($mtch[2],0,-1) : $mtch[2]);
+ $datarray['term'][] = array(
+ 'uid' => $importer['channel_id'],
+ 'type' => TERM_MENTION,
+ 'otype' => TERM_OBJ_POST,
+ 'term' => $term,
+ 'url' => $mtch[1]
+ );
+ }
+ }
+
+
+
+
+ $plink = service_plink($contact,$guid);
+
+ $datarray['aid'] = $importer['channel_account_id'];
+ $datarray['uid'] = $importer['channel_id'];
+
+ $datarray['verb'] = ACTIVITY_POST;
+ $datarray['mid'] = $datarray['parent_mid'] = $guid;
+
+ $datarray['changed'] = $datarray['created'] = $datarray['edited'] = datetime_convert('UTC','UTC',$created);
+ $datarray['item_private'] = $private;
+
+ $datarray['plink'] = $plink;
+
+ $datarray['author_xchan'] = $contact['xchan_hash'];
+ $datarray['owner_xchan'] = $contact['xchan_hash'];
+
+ $datarray['body'] = $body;
+
+ $datarray['app'] = $app;
+
+ $datarray['item_unseen'] = 1;
+ $datarray['item_thread_top'] = 1;
+
+ $tgroup = tgroup_check($importer['channel_id'],$datarray);
+
+ if((! $importer['system']) && (! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'send_stream')) && (! $tgroup)) {
+ logger('diaspora_post: Ignoring this author.');
+ return 202;
+ }
+
+ if($importer['system']) {
+ $datarray['comment_policy'] = 'network: diaspora';
+ }
+
+ if(! post_is_importable($datarray,$contact)) {
+ logger('diaspora_post: filtering this author.');
+ return 202;
+ }
+
+ $result = item_store($datarray);
+ return;
+
+}
+
+
+function get_diaspora_reshare_xml($url,$recurse = 0) {
+
+ $x = z_fetch_url($url);
+ if(! $x['success'])
+ $x = z_fetch_url(str_replace('https://','http://',$url));
+ if(! $x['success']) {
+ logger('get_diaspora_reshare_xml: unable to fetch source url ' . $url);
+ return;
+ }
+ logger('get_diaspora_reshare_xml: source: ' . $x['body'], LOGGER_DEBUG);
+
+ $source_xml = parse_xml_string($x['body'],false);
+
+ if(! $source_xml) {
+ logger('get_diaspora_reshare_xml: unparseable result from ' . $url);
+ return '';
+ }
+
+ if($source_xml->post->status_message) {
+ return $source_xml;
+ }
+
+ // see if it's a reshare of a reshare
+
+ if($source_xml->post->reshare)
+ $xml = $source_xml->post->reshare;
+ else
+ return false;
+
+ if($xml->root_diaspora_id && $xml->root_guid && $recurse < 15) {
+ $orig_author = notags(unxmlify($xml->root_diaspora_id));
+ $orig_guid = notags(unxmlify($xml->root_guid));
+ $source_url = 'https://' . substr($orig_author,strpos($orig_author,'@')+1) . '/p/' . $orig_guid . '.xml';
+ $y = get_diaspora_reshare_xml($source_url,$recurse+1);
+ if($y)
+ return $y;
+ }
+ return false;
+}
+
+
+
+function diaspora_reshare($importer,$xml,$msg) {
+
+ logger('diaspora_reshare: init: ' . print_r($xml,true), LOGGER_DATA);
+
+ $a = get_app();
+ $guid = notags(unxmlify($xml->guid));
+ $diaspora_handle = notags(unxmlify($xml->diaspora_handle));
+
+
+ if($diaspora_handle != $msg['author']) {
+ logger('diaspora_post: Potential forgery. Message handle is not the same as envelope sender.');
+ return 202;
+ }
+
+ $contact = diaspora_get_contact_by_handle($importer['channel_id'],$diaspora_handle);
+ if(! $contact)
+ return;
+
+ $search_guid = ((strlen($guid) == 64) ? $guid . '%' : $guid);
+ $r = q("SELECT id FROM item WHERE uid = %d AND mid like '%s' LIMIT 1",
+ intval($importer['channel_id']),
+ dbesc($search_guid)
+ );
+ if($r) {
+ logger('diaspora_reshare: message exists: ' . $guid);
+ return;
+ }
+
+ $orig_author = notags(unxmlify($xml->root_diaspora_id));
+ $orig_guid = notags(unxmlify($xml->root_guid));
+
+ $source_url = 'https://' . substr($orig_author,strpos($orig_author,'@')+1) . '/p/' . $orig_guid . '.xml';
+ $orig_url = 'https://'.substr($orig_author,strpos($orig_author,'@')+1).'/posts/'.$orig_guid;
+
+ $source_xml = get_diaspora_reshare_xml($source_url);
+
+ if($source_xml->post->status_message) {
+ $body = diaspora2bb($source_xml->post->status_message->raw_message);
+
+ $orig_author = notags(unxmlify($source_xml->post->status_message->diaspora_handle));
+ $orig_guid = notags(unxmlify($source_xml->post->status_message->guid));
+
+
+ // Checking for embedded pictures
+ if($source_xml->post->status_message->photo->remote_photo_path &&
+ $source_xml->post->status_message->photo->remote_photo_name) {
+
+ $remote_photo_path = notags(unxmlify($source_xml->post->status_message->photo->remote_photo_path));
+ $remote_photo_name = notags(unxmlify($source_xml->post->status_message->photo->remote_photo_name));
+
+ $body = '[img]'.$remote_photo_path.$remote_photo_name.'[/img]'."\n".$body;
+
+ logger('diaspora_reshare: embedded picture link found: '.$body, LOGGER_DEBUG);
+ }
+
+ $body = scale_external_images($body);
+
+ // Add OEmbed and other information to the body
+// $body = add_page_info_to_body($body, false, true);
+ }
+ else {
+ // Maybe it is a reshare of a photo that will be delivered at a later time (testing)
+ logger('diaspora_reshare: no reshare content found: ' . print_r($source_xml,true));
+ $body = "";
+ //return;
+ }
+
+ $maxlen = get_max_import_size();
+
+ if($maxlen && mb_strlen($body) > $maxlen) {
+ $body = mb_substr($body,0,$maxlen,'UTF-8');
+ logger('message length exceeds max_import_size: truncated');
+ }
+
+ $person = find_diaspora_person_by_handle($orig_author);
+
+ if($person) {
+ $orig_author_name = $person['xchan_name'];
+ $orig_author_link = $person['xchan_url'];
+ $orig_author_photo = $person['xchan_photo_m'];
+ }
+
+
+ $created = unxmlify($xml->created_at);
+ $private = ((unxmlify($xml->public) == 'false') ? 1 : 0);
+
+ $datarray = array();
+
+ // Look for tags and linkify them
+ $results = linkify_tags(get_app(), $body, $importer['channel_id'], true);
+
+ $datarray['term'] = array();
+
+ if($results) {
+ foreach($results as $result) {
+ $success = $result['success'];
+ if($success['replaced']) {
+ $datarray['term'][] = array(
+ 'uid' => $importer['channel_id'],
+ 'type' => $success['termtype'],
+ 'otype' => TERM_OBJ_POST,
+ 'term' => $success['term'],
+ 'url' => $success['url']
+ );
+ }
+ }
+ }
+
+ $cnt = preg_match_all('/@\[url=(.*?)\](.*?)\[\/url\]/ism',$body,$matches,PREG_SET_ORDER);
+ if($cnt) {
+ foreach($matches as $mtch) {
+ $datarray['term'][] = array(
+ 'uid' => $importer['channel_id'],
+ 'type' => TERM_MENTION,
+ 'otype' => TERM_OBJ_POST,
+ 'term' => $mtch[2],
+ 'url' => $mtch[1]
+ );
+ }
+ }
+
+ $cnt = preg_match_all('/@\[zrl=(.*?)\](.*?)\[\/zrl\]/ism',$body,$matches,PREG_SET_ORDER);
+ if($cnt) {
+ foreach($matches as $mtch) {
+ // don't include plustags in the term
+ $term = ((substr($mtch[2],-1,1) === '+') ? substr($mtch[2],0,-1) : $mtch[2]);
+ $datarray['term'][] = array(
+ 'uid' => $importer['channel_id'],
+ 'type' => TERM_MENTION,
+ 'otype' => TERM_OBJ_POST,
+ 'term' => $term,
+ 'url' => $mtch[1]
+ );
+ }
+ }
+
+
+
+
+
+ $newbody = "[share author='" . urlencode($orig_author_name)
+ . "' profile='" . $orig_author_link
+ . "' avatar='" . $orig_author_photo
+ . "' link='" . $orig_url
+ . "' posted='" . datetime_convert('UTC','UTC',unxmlify($source_xml->post->status_message->created_at))
+ . "' message_id='" . unxmlify($source_xml->post->status_message->guid)
+ . "']" . $body . "[/share]";
+
+
+ $plink = service_plink($contact,$guid);
+ $datarray['aid'] = $importer['channel_account_id'];
+ $datarray['uid'] = $importer['channel_id'];
+ $datarray['mid'] = $datarray['parent_mid'] = $guid;
+ $datarray['changed'] = $datarray['created'] = $datarray['edited'] = datetime_convert('UTC','UTC',$created);
+ $datarray['item_private'] = $private;
+ $datarray['plink'] = $plink;
+ $datarray['owner_xchan'] = $contact['xchan_hash'];
+ $datarray['author_xchan'] = $contact['xchan_hash'];
+
+ $datarray['body'] = $newbody;
+ $datarray['app'] = 'Diaspora';
+
+
+ $tgroup = tgroup_check($importer['channel_id'],$datarray);
+
+ if((! $importer['system']) && (! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'send_stream')) && (! $tgroup)) {
+ logger('diaspora_reshare: Ignoring this author.');
+ return 202;
+ }
+
+ if(! post_is_importable($datarray,$contact)) {
+ logger('diaspora_reshare: filtering this author.');
+ return 202;
+ }
+
+ $result = item_store($datarray);
+
+ return;
+
+}
+
+
+function diaspora_asphoto($importer,$xml,$msg) {
+ logger('diaspora_asphoto called');
+
+ $a = get_app();
+ $guid = notags(unxmlify($xml->guid));
+ $diaspora_handle = notags(unxmlify($xml->diaspora_handle));
+
+ if($diaspora_handle != $msg['author']) {
+ logger('diaspora_post: Potential forgery. Message handle is not the same as envelope sender.');
+ return 202;
+ }
+
+ $contact = diaspora_get_contact_by_handle($importer['channel_id'],$diaspora_handle);
+ if(! $contact)
+ return;
+
+ if((! $importer['system']) && (! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'send_stream'))) {
+ logger('diaspora_asphoto: Ignoring this author.');
+ return 202;
+ }
+
+ $message_id = $diaspora_handle . ':' . $guid;
+ $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `guid` = '%s' LIMIT 1",
+ intval($importer['channel_id']),
+ dbesc($message_id),
+ dbesc($guid)
+ );
+ if(count($r)) {
+ logger('diaspora_asphoto: message exists: ' . $guid);
+ return;
+ }
+
+ // allocate a guid on our system - we aren't fixing any collisions.
+ // we're ignoring them
+
+ $g = q("select * from guid where guid = '%s' limit 1",
+ dbesc($guid)
+ );
+ if(! count($g)) {
+ q("insert into guid ( guid ) values ( '%s' )",
+ dbesc($guid)
+ );
+ }
+
+ $created = unxmlify($xml->created_at);
+ $private = ((unxmlify($xml->public) == 'false') ? 1 : 0);
+
+ if(strlen($xml->objectId) && ($xml->objectId != 0) && ($xml->image_url)) {
+ $body = '[url=' . notags(unxmlify($xml->image_url)) . '][img]' . notags(unxmlify($xml->objectId)) . '[/img][/url]' . "\n";
+ $body = scale_external_images($body,false);
+ }
+ elseif($xml->image_url) {
+ $body = '[img]' . notags(unxmlify($xml->image_url)) . '[/img]' . "\n";
+ $body = scale_external_images($body);
+ }
+ else {
+ logger('diaspora_asphoto: no photo url found.');
+ return;
+ }
+
+ $plink = service_plink($contact,$guid);
+
+ $datarray = array();
+
+ $datarray['uid'] = $importer['channel_id'];
+ $datarray['contact-id'] = $contact['id'];
+ $datarray['wall'] = 0;
+ $datarray['network'] = NETWORK_DIASPORA;
+ $datarray['guid'] = $guid;
+ $datarray['uri'] = $datarray['parent-uri'] = $message_id;
+ $datarray['changed'] = $datarray['created'] = $datarray['edited'] = datetime_convert('UTC','UTC',$created);
+ $datarray['private'] = $private;
+ $datarray['parent'] = 0;
+ $datarray['plink'] = $plink;
+ $datarray['owner-name'] = $contact['name'];
+ $datarray['owner-link'] = $contact['url'];
+ //$datarray['owner-avatar'] = $contact['thumb'];
+ $datarray['owner-avatar'] = ((x($contact,'thumb')) ? $contact['thumb'] : $contact['photo']);
+ $datarray['author-name'] = $contact['name'];
+ $datarray['author-link'] = $contact['url'];
+ $datarray['author-avatar'] = $contact['thumb'];
+ $datarray['body'] = $body;
+
+ $datarray['app'] = 'Diaspora/Cubbi.es';
+
+ $message_id = item_store($datarray);
+
+ //if($message_id) {
+ // q("update item set plink = '%s' where id = %d",
+ // dbesc($a->get_baseurl() . '/display/' . $importer['nickname'] . '/' . $message_id),
+ // intval($message_id)
+ // );
+ //}
+
+ return;
+
+}
+
+
+
+
+
+
+function diaspora_comment($importer,$xml,$msg) {
+
+ $a = get_app();
+ $guid = notags(unxmlify($xml->guid));
+ $parent_guid = notags(unxmlify($xml->parent_guid));
+ $diaspora_handle = notags(unxmlify($xml->diaspora_handle));
+ $target_type = notags(unxmlify($xml->target_type));
+ $text = unxmlify($xml->text);
+ $author_signature = notags(unxmlify($xml->author_signature));
+
+ $parent_author_signature = (($xml->parent_author_signature) ? notags(unxmlify($xml->parent_author_signature)) : '');
+
+ $contact = diaspora_get_contact_by_handle($importer['channel_id'],$msg['author']);
+ if(! $contact) {
+ logger('diaspora_comment: cannot find contact: ' . $msg['author']);
+ return;
+ }
+
+
+
+ $pubcomment = get_pconfig($importer['channel_id'],'system','diaspora_public_comments');
+
+ // by default comments on public posts are allowed from anybody on Diaspora. That is their policy.
+ // Once this setting is set to something we'll track your preference and it will over-ride the default.
+
+ if($pubcomment === false)
+ $pubcomment = 1;
+
+ // Friendica is currently truncating guids at 64 chars
+ $search_guid = $parent_guid;
+ if(strlen($parent_guid) == 64)
+ $search_guid = $parent_guid . '%';
+
+ $r = q("SELECT * FROM item WHERE uid = %d AND mid LIKE '%s' LIMIT 1",
+ intval($importer['channel_id']),
+ dbesc($search_guid)
+ );
+ if(! $r) {
+ logger('diaspora_comment: parent item not found: parent: ' . $parent_guid . ' item: ' . $guid);
+ return;
+ }
+
+ $parent_item = $r[0];
+
+ if(intval($parent_item['item_private']))
+ $pubcomment = 0;
+
+ $search_guid = $guid;
+ if(strlen($guid) == 64)
+ $search_guid = $guid . '%';
+
+
+ $r = q("SELECT * FROM item WHERE uid = %d AND mid like '%s' LIMIT 1",
+ intval($importer['channel_id']),
+ dbesc($search_guid)
+ );
+ if($r) {
+ logger('diaspora_comment: our comment just got relayed back to us (or there was a guid collision) : ' . $guid);
+ return;
+ }
+
+
+
+ /* How Diaspora performs comment signature checking:
+
+ - If an item has been sent by the comment author to the top-level post owner to relay on
+ to the rest of the contacts on the top-level post, the top-level post owner should check
+ the author_signature, then create a parent_author_signature before relaying the comment on
+ - If an item has been relayed on by the top-level post owner, the contacts who receive it
+ check only the parent_author_signature. Basically, they trust that the top-level post
+ owner has already verified the authenticity of anything he/she sends out
+ - In either case, the signature that get checked is the signature created by the person
+ who sent the psuedo-salmon
+ */
+
+ $signed_data = $guid . ';' . $parent_guid . ';' . $text . ';' . $diaspora_handle;
+ $key = $msg['key'];
+
+ if($parent_author_signature) {
+ // If a parent_author_signature exists, then we've received the comment
+ // relayed from the top-level post owner. There's no need to check the
+ // author_signature if the parent_author_signature is valid
+
+ $parent_author_signature = base64_decode($parent_author_signature);
+
+ if(! rsa_verify($signed_data,$parent_author_signature,$key,'sha256')) {
+ logger('diaspora_comment: top-level owner verification failed.');
+ return;
+ }
+ }
+ else {
+ // If there's no parent_author_signature, then we've received the comment
+ // from the comment creator. In that case, the person is commenting on
+ // our post, so he/she must be a contact of ours and his/her public key
+ // should be in $msg['key']
+
+ if($importer['system']) {
+ // don't relay to the sys channel
+ logger('diaspora_comment: relay to sys channel blocked.');
+ return;
+ }
+
+ $author_signature = base64_decode($author_signature);
+
+ if(! rsa_verify($signed_data,$author_signature,$key,'sha256')) {
+ logger('diaspora_comment: comment author verification failed.');
+ return;
+ }
+ }
+
+ // Phew! Everything checks out. Now create an item.
+
+ // Find the original comment author information.
+ // We need this to make sure we display the comment author
+ // information (name and avatar) correctly.
+
+ if(strcasecmp($diaspora_handle,$msg['author']) == 0)
+ $person = $contact;
+ else {
+ $person = find_diaspora_person_by_handle($diaspora_handle);
+
+ if(! is_array($person)) {
+ logger('diaspora_comment: unable to find author details');
+ return;
+ }
+ }
+
+
+ $body = diaspora2bb($text);
+
+
+ $maxlen = get_max_import_size();
+
+ if($maxlen && mb_strlen($body) > $maxlen) {
+ $body = mb_substr($body,0,$maxlen,'UTF-8');
+ logger('message length exceeds max_import_size: truncated');
+ }
+
+
+ $datarray = array();
+
+ // Look for tags and linkify them
+ $results = linkify_tags(get_app(), $body, $importer['channel_id'], true);
+
+ $datarray['term'] = array();
+
+ if($results) {
+ foreach($results as $result) {
+ $success = $result['success'];
+ if($success['replaced']) {
+ $datarray['term'][] = array(
+ 'uid' => $importer['channel_id'],
+ 'type' => $success['termtype'],
+ 'otype' => TERM_OBJ_POST,
+ 'term' => $success['term'],
+ 'url' => $success['url']
+ );
+ }
+ }
+ }
+
+ $cnt = preg_match_all('/@\[url=(.*?)\](.*?)\[\/url\]/ism',$body,$matches,PREG_SET_ORDER);
+ if($cnt) {
+ foreach($matches as $mtch) {
+ $datarray['term'][] = array(
+ 'uid' => $importer['channel_id'],
+ 'type' => TERM_MENTION,
+ 'otype' => TERM_OBJ_POST,
+ 'term' => $mtch[2],
+ 'url' => $mtch[1]
+ );
+ }
+ }
+
+ $cnt = preg_match_all('/@\[zrl=(.*?)\](.*?)\[\/zrl\]/ism',$body,$matches,PREG_SET_ORDER);
+ if($cnt) {
+ foreach($matches as $mtch) {
+ // don't include plustags in the term
+ $term = ((substr($mtch[2],-1,1) === '+') ? substr($mtch[2],0,-1) : $mtch[2]);
+ $datarray['term'][] = array(
+ 'uid' => $importer['channel_id'],
+ 'type' => TERM_MENTION,
+ 'otype' => TERM_OBJ_POST,
+ 'term' => $term,
+ 'url' => $mtch[1]
+ );
+ }
+ }
+
+ $datarray['aid'] = $importer['channel_account_id'];
+ $datarray['uid'] = $importer['channel_id'];
+ $datarray['verb'] = ACTIVITY_POST;
+ $datarray['mid'] = $guid;
+ $datarray['parent_mid'] = $parent_item['mid'];
+
+ // set the route to that of the parent so downstream hubs won't reject it.
+ $datarray['route'] = $parent_item['route'];
+
+ // No timestamps for comments? OK, we'll the use current time.
+ $datarray['changed'] = $datarray['created'] = $datarray['edited'] = datetime_convert();
+ $datarray['item_private'] = $parent_item['item_private'];
+
+ $datarray['owner_xchan'] = $parent_item['owner_xchan'];
+ $datarray['author_xchan'] = $person['xchan_hash'];
+
+ $datarray['body'] = $body;
+
+ if(strstr($person['xchan_network'],'friendica'))
+ $app = 'Friendica';
+ else
+ $app = 'Diaspora';
+
+ $datarray['app'] = $app;
+
+ if(! $parent_author_signature) {
+ $key = get_config('system','pubkey');
+ $x = array('signer' => $diaspora_handle, 'body' => $text,
+ 'signed_text' => $signed_data, 'signature' => base64_encode($author_signature));
+ $datarray['diaspora_meta'] = json_encode($x);
+ }
+
+
+
+ // So basically if something arrives at the sys channel it's by definition public and we allow it.
+ // If $pubcomment and the parent was public, we allow it.
+ // In all other cases, honour the permissions for this Diaspora connection
+
+ $tgroup = tgroup_check($importer['channel_id'],$datarray);
+
+ if((! $importer['system']) && (! $pubcomment) && (! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'post_comments')) && (! $tgroup)) {
+ logger('diaspora_comment: Ignoring this author.');
+ return 202;
+ }
+
+
+
+
+ $result = item_store($datarray);
+
+ if($result && $result['success'])
+ $message_id = $result['item_id'];
+
+ if(intval($parent_item['item_origin']) && (! $parent_author_signature)) {
+ // if the message isn't already being relayed, notify others
+ // the existence of parent_author_signature means the parent_author or owner
+ // is already relaying.
+
+ proc_run('php','include/notifier.php','comment-import',$message_id);
+ }
+
+ if($result['item_id']) {
+ $r = q("select * from item where id = %d limit 1",
+ intval($result['item_id'])
+ );
+ if($r)
+ send_status_notifications($result['item_id'],$r[0]);
+ }
+
+ return;
+}
+
+
+
+
+function diaspora_conversation($importer,$xml,$msg) {
+
+ $a = get_app();
+
+ $guid = notags(unxmlify($xml->guid));
+ $subject = notags(unxmlify($xml->subject));
+ $diaspora_handle = notags(unxmlify($xml->diaspora_handle));
+ $participant_handles = notags(unxmlify($xml->participant_handles));
+ $created_at = datetime_convert('UTC','UTC',notags(unxmlify($xml->created_at)));
+
+ $parent_uri = $diaspora_handle . ':' . $guid;
+
+ $messages = $xml->message;
+
+ if(! count($messages)) {
+ logger('diaspora_conversation: empty conversation');
+ return;
+ }
+
+ $contact = diaspora_get_contact_by_handle($importer['channel_id'],$msg['author']);
+ if(! $contact) {
+ logger('diaspora_conversation: cannot find contact: ' . $msg['author']);
+ return;
+ }
+
+
+ if(! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'post_mail')) {
+ logger('diaspora_conversation: Ignoring this author.');
+ return 202;
+ }
+
+ $conversation = null;
+
+ $c = q("select * from conv where uid = %d and guid = '%s' limit 1",
+ intval($importer['channel_id']),
+ dbesc($guid)
+ );
+ if(count($c))
+ $conversation = $c[0];
+ else {
+ if($subject)
+ $nsubject = str_rot47(base64url_encode($subject));
+
+ $r = q("insert into conv (uid,guid,creator,created,updated,subject,recips) values(%d, '%s', '%s', '%s', '%s', '%s', '%s') ",
+ intval($importer['channel_id']),
+ dbesc($guid),
+ dbesc($diaspora_handle),
+ dbesc(datetime_convert('UTC','UTC',$created_at)),
+ dbesc(datetime_convert()),
+ dbesc($nsubject),
+ dbesc($participant_handles)
+ );
+ if($r)
+ $c = q("select * from conv where uid = %d and guid = '%s' limit 1",
+ intval($importer['channel_id']),
+ dbesc($guid)
+ );
+ if(count($c))
+ $conversation = $c[0];
+ }
+ if(! $conversation) {
+ logger('diaspora_conversation: unable to create conversation.');
+ return;
+ }
+
+ $conversation['subject'] = base64url_decode(str_rot47($conversation['subject']));
+
+ foreach($messages as $mesg) {
+
+ $reply = 0;
+
+ $msg_guid = notags(unxmlify($mesg->guid));
+ $msg_parent_guid = notags(unxmlify($mesg->parent_guid));
+ $msg_parent_author_signature = notags(unxmlify($mesg->parent_author_signature));
+ $msg_author_signature = notags(unxmlify($mesg->author_signature));
+ $msg_text = unxmlify($mesg->text);
+ $msg_created_at = datetime_convert('UTC','UTC',notags(unxmlify($mesg->created_at)));
+ $msg_diaspora_handle = notags(unxmlify($mesg->diaspora_handle));
+ $msg_conversation_guid = notags(unxmlify($mesg->conversation_guid));
+ if($msg_conversation_guid != $guid) {
+ logger('diaspora_conversation: message conversation guid does not belong to the current conversation. ' . $xml);
+ continue;
+ }
+
+ $body = diaspora2bb($msg_text);
+
+
+ $maxlen = get_max_import_size();
+
+ if($maxlen && mb_strlen($body) > $maxlen) {
+ $body = mb_substr($body,0,$maxlen,'UTF-8');
+ logger('message length exceeds max_import_size: truncated');
+ }
+
+
+ $author_signed_data = $msg_guid . ';' . $msg_parent_guid . ';' . $msg_text . ';' . unxmlify($mesg->created_at) . ';' . $msg_diaspora_handle . ';' . $msg_conversation_guid;
+
+ $author_signature = base64_decode($msg_author_signature);
+
+ if(strcasecmp($msg_diaspora_handle,$msg['author']) == 0) {
+ $person = $contact;
+ $key = $msg['key'];
+ }
+ else {
+ $person = find_diaspora_person_by_handle($msg_diaspora_handle);
+
+ if(is_array($person) && x($person,'xchan_pubkey'))
+ $key = $person['xchan_pubkey'];
+ else {
+ logger('diaspora_conversation: unable to find author details');
+ continue;
+ }
+ }
+
+ if(! rsa_verify($author_signed_data,$author_signature,$key,'sha256')) {
+ logger('diaspora_conversation: verification failed.');
+ continue;
+ }
+
+ if($msg_parent_author_signature) {
+ $owner_signed_data = $msg_guid . ';' . $msg_parent_guid . ';' . $msg_text . ';' . unxmlify($mesg->created_at) . ';' . $msg_diaspora_handle . ';' . $msg_conversation_guid;
+
+ $parent_author_signature = base64_decode($msg_parent_author_signature);
+
+ $key = $msg['key'];
+
+ if(! rsa_verify($owner_signed_data,$parent_author_signature,$key,'sha256')) {
+ logger('diaspora_conversation: owner verification failed.');
+ continue;
+ }
+ }
+
+ $r = q("select id from mail where mid = '%s' limit 1",
+ dbesc($message_id)
+ );
+ if(count($r)) {
+ logger('diaspora_conversation: duplicate message already delivered.', LOGGER_DEBUG);
+ continue;
+ }
+
+ if($subject)
+ $subject = str_rot47(base64url_encode($subject));
+ if($body)
+ $body = str_rot47(base64url_encode($body));
+
+ q("insert into mail ( `channel_id`, `convid`, `conv_guid`, `from_xchan`,`to_xchan`,`title`,`body`,`mail_obscured`,`mid`,`parent_mid`,`created`) values ( %d, %d, '%s', '%s', '%s', '%s', '%s', %d, '%s', '%s', '%s')",
+ intval($importer['channel_id']),
+ intval($conversation['id']),
+ intval($conversation['guid']),
+ dbesc($person['xchan_hash']),
+ dbesc($importer['channel_hash']),
+ dbesc($subject),
+ dbesc($body),
+ intval(1),
+ dbesc($msg_guid),
+ dbesc($parent_uri),
+ dbesc($msg_created_at)
+ );
+
+ q("update conv set updated = '%s' where id = %d",
+ dbesc(datetime_convert()),
+ intval($conversation['id'])
+ );
+
+ require_once('include/enotify.php');
+/******
+//FIXME
+
+ notification(array(
+ 'type' => NOTIFY_MAIL,
+ 'notify_flags' => $importer['notify-flags'],
+ 'language' => $importer['language'],
+ 'to_name' => $importer['username'],
+ 'to_email' => $importer['email'],
+ 'uid' =>$importer['importer_uid'],
+ 'item' => array('subject' => $subject, 'body' => $body),
+ 'source_name' => $person['name'],
+ 'source_link' => $person['url'],
+ 'source_photo' => $person['thumb'],
+ 'verb' => ACTIVITY_POST,
+ 'otype' => 'mail'
+ ));
+*******/
+
+ }
+
+ return;
+}
+
+function diaspora_message($importer,$xml,$msg) {
+
+ $a = get_app();
+
+ $msg_guid = notags(unxmlify($xml->guid));
+ $msg_parent_guid = notags(unxmlify($xml->parent_guid));
+ $msg_parent_author_signature = notags(unxmlify($xml->parent_author_signature));
+ $msg_author_signature = notags(unxmlify($xml->author_signature));
+ $msg_text = unxmlify($xml->text);
+ $msg_created_at = datetime_convert('UTC','UTC',notags(unxmlify($xml->created_at)));
+ $msg_diaspora_handle = notags(unxmlify($xml->diaspora_handle));
+ $msg_conversation_guid = notags(unxmlify($xml->conversation_guid));
+
+ $parent_uri = $msg_parent_guid;
+
+ $contact = diaspora_get_contact_by_handle($importer['channel_id'],$msg_diaspora_handle);
+ if(! $contact) {
+ logger('diaspora_message: cannot find contact: ' . $msg_diaspora_handle);
+ return;
+ }
+
+ if(($contact['rel'] == CONTACT_IS_FOLLOWER) || ($contact['blocked']) || ($contact['readonly'])) {
+ logger('diaspora_message: Ignoring this author.');
+ return 202;
+ }
+
+ $conversation = null;
+
+ $c = q("select * from conv where uid = %d and guid = '%s' limit 1",
+ intval($importer['channel_id']),
+ dbesc($msg_conversation_guid)
+ );
+ if($c)
+ $conversation = $c[0];
+ else {
+ logger('diaspora_message: conversation not available.');
+ return;
+ }
+
+ $reply = 0;
+
+ $subject = $conversation['subject'];
+ $body = diaspora2bb($msg_text);
+
+
+ $maxlen = get_max_import_size();
+
+ if($maxlen && mb_strlen($body) > $maxlen) {
+ $body = mb_substr($body,0,$maxlen,'UTF-8');
+ logger('message length exceeds max_import_size: truncated');
+ }
+
+
+
+ $message_id = $msg_diaspora_handle . ':' . $msg_guid;
+
+ $author_signed_data = $msg_guid . ';' . $msg_parent_guid . ';' . $msg_text . ';' . unxmlify($xml->created_at) . ';' . $msg_diaspora_handle . ';' . $msg_conversation_guid;
+
+
+ $author_signature = base64_decode($msg_author_signature);
+
+ $person = find_diaspora_person_by_handle($msg_diaspora_handle);
+ if(is_array($person) && x($person,'xchan_pubkey'))
+ $key = $person['xchan_pubkey'];
+ else {
+ logger('diaspora_message: unable to find author details');
+ return;
+ }
+
+ if(! rsa_verify($author_signed_data,$author_signature,$key,'sha256')) {
+ logger('diaspora_message: verification failed.');
+ return;
+ }
+
+ $r = q("select id from mail where mid = '%s' and channel_id = %d limit 1",
+ dbesc($message_id),
+ intval($importer['channel_id'])
+ );
+ if($r) {
+ logger('diaspora_message: duplicate message already delivered.', LOGGER_DEBUG);
+ return;
+ }
+
+ $key = get_config('system','pubkey');
+ if($subject)
+ $subject = str_rot47(base64url_encode($subject));
+ if($body)
+ $body = str_rot47(base64url_encode($body));
+
+ q("insert into mail ( `channel_id`, `convid`, `conv_guid`, `from_xchan`,`to_xchan`,`title`,`body`,`mail_obscured`,`mid`,`parent_mid`,`created`) values ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%d','%s','%s','%s')",
+ intval($importer['channel_id']),
+ intval($conversation['id']),
+ intval($conversation['guid']),
+ dbesc($person['xchan_hash']),
+ dbesc($importer['xchan_hash']),
+ dbesc($subject),
+ dbesc($body),
+ intval(1),
+ dbesc($msg_guid),
+ dbesc($parent_uri),
+ dbesc($msg_created_at)
+ );
+
+ q("update conv set updated = '%s' where id = %d",
+ dbesc(datetime_convert()),
+ intval($conversation['id'])
+ );
+
+ return;
+}
+
+
+function diaspora_photo($importer,$xml,$msg) {
+
+ $a = get_app();
+
+ logger('diaspora_photo: init',LOGGER_DEBUG);
+
+ $remote_photo_path = notags(unxmlify($xml->remote_photo_path));
+
+ $remote_photo_name = notags(unxmlify($xml->remote_photo_name));
+
+ $status_message_guid = notags(unxmlify($xml->status_message_guid));
+
+ $guid = notags(unxmlify($xml->guid));
+
+ $diaspora_handle = notags(unxmlify($xml->diaspora_handle));
+
+ $public = notags(unxmlify($xml->public));
+
+ $created_at = notags(unxmlify($xml_created_at));
+
+ logger('diaspora_photo: status_message_guid: ' . $status_message_guid, LOGGER_DEBUG);
+
+ $contact = diaspora_get_contact_by_handle($importer['channel_id'],$msg['author']);
+ if(! $contact) {
+ logger('diaspora_photo: contact record not found: ' . $msg['author'] . ' handle: ' . $diaspora_handle);
+ return;
+ }
+
+ if((! $importer['system']) && (! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'send_stream'))) {
+ logger('diaspora_photo: Ignoring this author.');
+ return 202;
+ }
+
+ $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `mid` = '%s' LIMIT 1",
+ intval($importer['channel_id']),
+ dbesc($status_message_guid)
+ );
+ if(! $r) {
+ logger('diaspora_photo: attempt = ' . $attempt . '; status message not found: ' . $status_message_guid . ' for photo: ' . $guid);
+ return;
+ }
+
+// $parent_item = $r[0];
+
+// $link_text = '[img]' . $remote_photo_path . $remote_photo_name . '[/img]' . "\n";
+
+// $link_text = scale_external_images($link_text, true,
+// array($remote_photo_name, 'scaled_full_' . $remote_photo_name));
+
+// if(strpos($parent_item['body'],$link_text) === false) {
+// $r = q("update item set `body` = '%s', `visible` = 1 where `id` = %d and `uid` = %d",
+// dbesc($link_text . $parent_item['body']),
+// intval($parent_item['id']),
+// intval($parent_item['uid'])
+// );
+// }
+
+ return;
+}
+
+
+
+
+function diaspora_like($importer,$xml,$msg) {
+
+ $a = get_app();
+ $guid = notags(unxmlify($xml->guid));
+ $parent_guid = notags(unxmlify($xml->parent_guid));
+ $diaspora_handle = notags(unxmlify($xml->diaspora_handle));
+ $target_type = notags(unxmlify($xml->target_type));
+ $positive = notags(unxmlify($xml->positive));
+ $author_signature = notags(unxmlify($xml->author_signature));
+
+ $parent_author_signature = (($xml->parent_author_signature) ? notags(unxmlify($xml->parent_author_signature)) : '');
+
+ // likes on comments not supported here and likes on photos not supported by Diaspora
+
+// if($target_type !== 'Post')
+// return;
+
+ $contact = diaspora_get_contact_by_handle($importer['channel_id'],$msg['author']);
+ if(! $contact) {
+ logger('diaspora_like: cannot find contact: ' . $msg['author'] . ' for channel ' . $importer['channel_name']);
+ return;
+ }
+
+
+ if((! $importer['system']) && (! perm_is_allowed($importer['channel_id'],$contact['xchan_hash'],'post_comments'))) {
+ logger('diaspora_like: Ignoring this author.');
+ return 202;
+ }
+
+ $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `mid` = '%s' LIMIT 1",
+ intval($importer['channel_id']),
+ dbesc($parent_guid)
+ );
+ if(! count($r)) {
+ logger('diaspora_like: parent item not found: ' . $guid);
+ return;
+ }
+
+ $parent_item = $r[0];
+
+ $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `mid` = '%s' LIMIT 1",
+ intval($importer['channel_id']),
+ dbesc($guid)
+ );
+ if(count($r)) {
+ if($positive === 'true') {
+ logger('diaspora_like: duplicate like: ' . $guid);
+ return;
+ }
+ // Note: I don't think "Like" objects with positive = "false" are ever actually used
+ // It looks like "RelayableRetractions" are used for "unlike" instead
+ if($positive === 'false') {
+ logger('diaspora_like: received a like with positive set to "false"...ignoring');
+ // perhaps call drop_item()
+ // FIXME--actually don't unless it turns out that Diaspora does indeed send out "false" likes
+ // send notification via proc_run()
+ return;
+ }
+ }
+
+ $i = q("select * from xchan where xchan_hash = '%s' limit 1",
+ dbesc($parent_item['author_xchan'])
+ );
+ if($i)
+ $item_author = $i[0];
+
+ // Note: I don't think "Like" objects with positive = "false" are ever actually used
+ // It looks like "RelayableRetractions" are used for "unlike" instead
+ if($positive === 'false') {
+ logger('diaspora_like: received a like with positive set to "false"');
+ logger('diaspora_like: unlike received with no corresponding like...ignoring');
+ return;
+ }
+
+
+ /* How Diaspora performs "like" signature checking:
+
+ - If an item has been sent by the like author to the top-level post owner to relay on
+ to the rest of the contacts on the top-level post, the top-level post owner should check
+ the author_signature, then create a parent_author_signature before relaying the like on
+ - If an item has been relayed on by the top-level post owner, the contacts who receive it
+ check only the parent_author_signature. Basically, they trust that the top-level post
+ owner has already verified the authenticity of anything he/she sends out
+ - In either case, the signature that get checked is the signature created by the person
+ who sent the salmon
+ */
+
+ // 2014-09-10 let's try this: signatures are failing. I'll try and make a signable string from
+ // the parameters in the order they were presented in the post. This is how D* creates the signable string.
+
+
+ $signed_data = $positive . ';' . $guid . ';' . $target_type . ';' . $parent_guid . ';' . $diaspora_handle;
+
+ $key = $msg['key'];
+
+ if($parent_author_signature) {
+ // If a parent_author_signature exists, then we've received the like
+ // relayed from the top-level post owner. There's no need to check the
+ // author_signature if the parent_author_signature is valid
+
+ $parent_author_signature = base64_decode($parent_author_signature);
+
+ if(! rsa_verify($signed_data,$parent_author_signature,$key,'sha256')) {
+ if (intval(get_config('system','ignore_diaspora_like_signature')))
+ logger('diaspora_like: top-level owner verification failed. Proceeding anyway.');
+ else {
+ logger('diaspora_like: top-level owner verification failed.');
+ return;
+ }
+ }
+ }
+ else {
+ // If there's no parent_author_signature, then we've received the like
+ // from the like creator. In that case, the person is "like"ing
+ // our post, so he/she must be a contact of ours and his/her public key
+ // should be in $msg['key']
+
+ $author_signature = base64_decode($author_signature);
+
+ if(! rsa_verify($signed_data,$author_signature,$key,'sha256')) {
+ if (intval(get_config('system','ignore_diaspora_like_signature')))
+ logger('diaspora_like: like creator verification failed. Proceeding anyway');
+ else {
+ logger('diaspora_like: like creator verification failed.');
+ return;
+ }
+ }
+ }
+
+ logger('diaspora_like: signature check complete.',LOGGER_DEBUG);
+
+ // Phew! Everything checks out. Now create an item.
+
+ // Find the original comment author information.
+ // We need this to make sure we display the comment author
+ // information (name and avatar) correctly.
+ if(strcasecmp($diaspora_handle,$msg['author']) == 0)
+ $person = $contact;
+ else {
+ $person = find_diaspora_person_by_handle($diaspora_handle);
+
+ if(! is_array($person)) {
+ logger('diaspora_like: unable to find author details');
+ return;
+ }
+ }
+
+ $uri = $diaspora_handle . ':' . $guid;
+
+ $activity = ACTIVITY_LIKE;
+
+ $post_type = (($parent_item['resource_type'] === 'photo') ? t('photo') : t('status'));
+
+ $links = array(array('rel' => 'alternate','type' => 'text/html', 'href' => $parent_item['plink']));
+ $objtype = (($parent_item['resource_type'] === 'photo') ? ACTIVITY_OBJ_PHOTO : ACTIVITY_OBJ_NOTE );
+
+ $body = $parent_item['body'];
+
+
+ $object = json_encode(array(
+ 'type' => $post_type,
+ 'id' => $parent_item['mid'],
+ 'parent' => (($parent_item['thr_parent']) ? $parent_item['thr_parent'] : $parent_item['parent_mid']),
+ 'link' => $links,
+ 'title' => $parent_item['title'],
+ 'content' => $parent_item['body'],
+ 'created' => $parent_item['created'],
+ 'edited' => $parent_item['edited'],
+ 'author' => array(
+ 'name' => $item_author['xchan_name'],
+ 'address' => $item_author['xchan_addr'],
+ 'guid' => $item_author['xchan_guid'],
+ 'guid_sig' => $item_author['xchan_guid_sig'],
+ 'link' => array(
+ array('rel' => 'alternate', 'type' => 'text/html', 'href' => $item_author['xchan_url']),
+ array('rel' => 'photo', 'type' => $item_author['xchan_photo_mimetype'], 'href' => $item_author['xchan_photo_m'])),
+ ),
+ ));
+
+
+ $bodyverb = t('%1$s likes %2$s\'s %3$s');
+
+ $arr = array();
+
+ $arr['uid'] = $importer['channel_id'];
+ $arr['aid'] = $importer['channel_account_id'];
+ $arr['mid'] = $guid;
+ $arr['parent_mid'] = $parent_item['mid'];
+ $arr['owner_xchan'] = $parent_item['owner_xchan'];
+ $arr['author_xchan'] = $person['xchan_hash'];
+
+ $ulink = '[url=' . $contact['url'] . ']' . $contact['name'] . '[/url]';
+ $alink = '[url=' . $parent_item['author-link'] . ']' . $parent_item['author-name'] . '[/url]';
+ $plink = '[url='. z_root() .'/display/'.$guid.']'.$post_type.'[/url]';
+ $arr['body'] = sprintf( $bodyverb, $ulink, $alink, $plink );
+
+ $arr['app'] = 'Diaspora';
+
+ // set the route to that of the parent so downstream hubs won't reject it.
+ $arr['route'] = $parent_item['route'];
+
+ $arr['item_private'] = $parent_item['item_private'];
+ $arr['verb'] = $activity;
+ $arr['obj_type'] = $objtype;
+ $arr['object'] = $object;
+
+ if(! $parent_author_signature) {
+ $key = get_config('system','pubkey');
+ $x = array('signer' => $diaspora_handle, 'body' => $text,
+ 'signed_text' => $signed_data, 'signature' => base64_encode($author_signature));
+ $arr['diaspora_meta'] = json_encode($x);
+ }
+
+ $x = item_store($arr);
+
+ if($x)
+ $message_id = $x['item_id'];
+
+ // if the message isn't already being relayed, notify others
+ // the existence of parent_author_signature means the parent_author or owner
+ // is already relaying. The parent_item['origin'] indicates the message was created on our system
+
+ if(intval($parent_item['item_origin']) && (! $parent_author_signature))
+ proc_run('php','include/notifier.php','comment-import',$message_id);
+
+ return;
+}
+
+function diaspora_retraction($importer,$xml) {
+
+
+ $guid = notags(unxmlify($xml->guid));
+ $diaspora_handle = notags(unxmlify($xml->diaspora_handle));
+ $type = notags(unxmlify($xml->type));
+
+ $contact = diaspora_get_contact_by_handle($importer['channel_id'],$diaspora_handle);
+ if(! $contact)
+ return;
+
+ if($type === 'Person') {
+ require_once('include/Contact.php');
+ contact_remove($importer['channel_id'],$contact['abook_id']);
+ }
+ elseif($type === 'Post') {
+ $r = q("select * from item where mid = '%s' and uid = %d limit 1",
+ dbesc('guid'),
+ intval($importer['channel_id'])
+ );
+ if(count($r)) {
+ if(link_compare($r[0]['author_xchan'],$contact['xchan_hash'])) {
+ drop_item($r[0]['id'],false);
+ }
+ }
+ }
+
+ return 202;
+ // NOTREACHED
+}
+
+function diaspora_signed_retraction($importer,$xml,$msg) {
+
+
+ $guid = notags(unxmlify($xml->target_guid));
+ $diaspora_handle = notags(unxmlify($xml->sender_handle));
+ $type = notags(unxmlify($xml->target_type));
+ $sig = notags(unxmlify($xml->target_author_signature));
+
+ $parent_author_signature = (($xml->parent_author_signature) ? notags(unxmlify($xml->parent_author_signature)) : '');
+
+ $contact = diaspora_get_contact_by_handle($importer['channel_id'],$diaspora_handle);
+ if(! $contact) {
+ logger('diaspora_signed_retraction: no contact ' . $diaspora_handle . ' for ' . $importer['channel_id']);
+ return;
+ }
+
+
+ $signed_data = $guid . ';' . $type ;
+ $key = $msg['key'];
+
+ /* How Diaspora performs relayable_retraction signature checking:
+
+ - If an item has been sent by the item author to the top-level post owner to relay on
+ to the rest of the contacts on the top-level post, the top-level post owner checks
+ the author_signature, then creates a parent_author_signature before relaying the item on
+ - If an item has been relayed on by the top-level post owner, the contacts who receive it
+ check only the parent_author_signature. Basically, they trust that the top-level post
+ owner has already verified the authenticity of anything he/she sends out
+ - In either case, the signature that get checked is the signature created by the person
+ who sent the salmon
+ */
+
+ if($parent_author_signature) {
+
+ $parent_author_signature = base64_decode($parent_author_signature);
+
+ if(! rsa_verify($signed_data,$parent_author_signature,$key,'sha256')) {
+ logger('diaspora_signed_retraction: top-level post owner verification failed');
+ return;
+ }
+
+ }
+ else {
+
+ $sig_decode = base64_decode($sig);
+
+ if(! rsa_verify($signed_data,$sig_decode,$key,'sha256')) {
+ logger('diaspora_signed_retraction: retraction owner verification failed.' . print_r($msg,true));
+ return;
+ }
+ }
+
+ if($type === 'StatusMessage' || $type === 'Comment' || $type === 'Like') {
+ $r = q("select * from item where mid = '%s' and uid = %d limit 1",
+ dbesc($guid),
+ intval($importer['channel_id'])
+ );
+ if($r) {
+ if($r[0]['author_xchan'] == $contact['xchan_hash']) {
+
+ drop_item($r[0]['id'],false, DROPITEM_PHASE1);
+
+ // Now check if the retraction needs to be relayed by us
+ //
+ // The first item in the `item` table with the parent id is the parent. However, MySQL doesn't always
+ // return the items ordered by `item`.`id`, in which case the wrong item is chosen as the parent.
+ // The only item with `parent` and `id` as the parent id is the parent item.
+ $p = q("select item_flags from item where parent = %d and id = %d limit 1",
+ $r[0]['parent'],
+ $r[0]['parent']
+ );
+ if($p) {
+ if(intval($p[0]['item_origin']) && (! $parent_author_signature)) {
+
+ // the existence of parent_author_signature would have meant the parent_author or owner
+ // is already relaying.
+
+ logger('diaspora_signed_retraction: relaying relayable_retraction');
+ proc_run('php','include/notifier.php','drop',$r[0]['id']);
+ }
+ }
+ }
+ }
+ }
+ else
+ logger('diaspora_signed_retraction: unknown type: ' . $type);
+
+ return 202;
+ // NOTREACHED
+}
+
+function diaspora_profile($importer,$xml,$msg) {
+
+ $a = get_app();
+ $diaspora_handle = notags(unxmlify($xml->diaspora_handle));
+
+
+ if($diaspora_handle != $msg['author']) {
+ logger('diaspora_post: Potential forgery. Message handle is not the same as envelope sender.');
+ return 202;
+ }
+
+ $contact = diaspora_get_contact_by_handle($importer['channel_id'],$diaspora_handle);
+ if(! $contact)
+ return;
+
+ if($contact['blocked']) {
+ logger('diaspora_post: Ignoring this author.');
+ return 202;
+ }
+
+ $name = unxmlify($xml->first_name) . ((strlen($xml->last_name)) ? ' ' . unxmlify($xml->last_name) : '');
+ $image_url = unxmlify($xml->image_url);
+ $birthday = unxmlify($xml->birthday);
+
+
+ $handle_parts = explode("@", $diaspora_handle);
+ if($name === '') {
+ $name = $handle_parts[0];
+ }
+
+ if( preg_match("|^https?://|", $image_url) === 0) {
+ $image_url = "http://" . $handle_parts[1] . $image_url;
+ }
+
+ require_once('include/photo/photo_driver.php');
+
+ $images = import_xchan_photo($image_url,$contact['xchan_hash']);
+
+ // Generic birthday. We don't know the timezone. The year is irrelevant.
+
+ $birthday = str_replace('1000','1901',$birthday);
+
+ $birthday = datetime_convert('UTC','UTC',$birthday,'Y-m-d');
+
+ // this is to prevent multiple birthday notifications in a single year
+ // if we already have a stored birthday and the 'm-d' part hasn't changed, preserve the entry, which will preserve the notify year
+
+ if(substr($birthday,5) === substr($contact['bd'],5))
+ $birthday = $contact['bd'];
+
+ $r = q("update xchan set xchan_name = '%s', xchan_name_date = '%s', xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s', xchan_photo_mimetype = '%s' where xchan_hash = '%s' ",
+ dbesc($name),
+ dbesc(datetime_convert()),
+ dbesc($images[0]),
+ dbesc($images[1]),
+ dbesc($images[2]),
+ dbesc($images[3]),
+ dbesc(datetime_convert()),
+ intval($contact['xchan_hash'])
+ );
+
+ return;
+
+}
+
+function diaspora_share($owner,$contact) {
+ $a = get_app();
+
+ $allowed = get_pconfig($owner['channel_id'],'system','diaspora_allowed');
+ if($allowed === false)
+ $allowed = 1;
+
+ if(! intval($allowed)) {
+ logger('diaspora_share: disallowed for channel ' . $importer['channel_name']);
+ return;
+ }
+
+
+
+ $myaddr = $owner['channel_address'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3);
+
+ if(! array_key_exists('xchan_hash',$contact)) {
+ $c = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where xchan_hash = '%s' limit 1",
+ dbesc($contact['hubloc_hash'])
+ );
+ if(! $c) {
+ logger('diaspora_share: ' . $contact['hubloc_hash'] . ' not found.');
+ return;
+ }
+ $contact = $c[0];
+ }
+
+ $theiraddr = $contact['xchan_addr'];
+
+ $tpl = get_markup_template('diaspora_share.tpl');
+ $msg = replace_macros($tpl, array(
+ '$sender' => $myaddr,
+ '$recipient' => $theiraddr
+ ));
+
+ $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['channel_prvkey'],$contact['xchan_pubkey'])));
+ return(diaspora_queue($owner,$contact,$slap, false));
+}
+
+function diaspora_unshare($owner,$contact) {
+
+ $a = get_app();
+ $myaddr = $owner['channel_address'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3);
+
+ $tpl = get_markup_template('diaspora_retract.tpl');
+ $msg = replace_macros($tpl, array(
+ '$guid' => $owner['channel_guid'] . str_replace('.','',get_app()->get_hostname()),
+ '$type' => 'Person',
+ '$handle' => $myaddr
+ ));
+
+ $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['channel_prvkey'],$contact['xchan_pubkey'])));
+
+ return(diaspora_queue($owner,$contact,$slap, false));
+}
+
+
+function diaspora_send_status($item,$owner,$contact,$public_batch = false) {
+
+ $a = get_app();
+ $myaddr = $owner['channel_address'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3);
+
+ if(intval($item['id']) != intval($item['parent'])) {
+ logger('attempted to send a comment as a top-level post');
+ return;
+ }
+
+ $images = array();
+
+ $title = $item['title'];
+ $body = bb2diaspora_itembody($item,true);
+
+/*
+ // We're trying to match Diaspora's split message/photo protocol but
+ // all the photos are displayed on D* as links and not img's - even
+ // though we're sending pretty much precisely what they send us when
+ // doing the same operation.
+ // Commented out for now, we'll use bb2diaspora to convert photos to markdown
+ // which seems to get through intact.
+
+ $cnt = preg_match_all('|\[img\](.*?)\[\/img\]|',$body,$matches,PREG_SET_ORDER);
+ if($cnt) {
+ foreach($matches as $mtch) {
+ $detail = array();
+ $detail['str'] = $mtch[0];
+ $detail['path'] = dirname($mtch[1]) . '/';
+ $detail['file'] = basename($mtch[1]);
+ $detail['guid'] = $item['guid'];
+ $detail['handle'] = $myaddr;
+ $images[] = $detail;
+ $body = str_replace($detail['str'],$mtch[1],$body);
+ }
+ }
+*/
+
+ if(intval($item['item_consensus'])) {
+ $poll = replace_macros(get_markup_template('diaspora_consensus.tpl'), array(
+ '$guid_q' => '10000000',
+ '$question' => t('Please choose'),
+ '$guid_y' => '00000001',
+ '$agree' => t('Agree'),
+ '$guid_n' => '0000000F',
+ '$disagree' => t('Disagree'),
+ '$guid_a' => '00000000',
+ '$abstain' => t('Abstain')
+ ));
+ }
+ elseif($item['resource_type'] === 'event' && $item['resource_id']) {
+ $poll = replace_macros(get_markup_template('diaspora_consensus.tpl'), array(
+ '$guid_q' => '1000000',
+ '$question' => t('Please choose'),
+ '$guid_y' => '0000001',
+ '$agree' => t('I will attend'),
+ '$guid_n' => '000000F',
+ '$disagree' => t('I will not attend'),
+ '$guid_a' => '0000000',
+ '$abstain' => t('I may attend')
+ ));
+ }
+ else
+ $poll = '';
+
+ $public = (($item['item_private']) ? 'false' : 'true');
+
+ require_once('include/datetime.php');
+ $created = datetime_convert('UTC','UTC',$item['created'],'Y-m-d H:i:s \U\T\C');
+
+ // Detect a share element and do a reshare
+ // see: https://github.com/Raven24/diaspora-federation/blob/master/lib/diaspora-federation/entities/reshare.rb
+ if (!$item['item_private'] AND ($ret = diaspora_is_reshare($item["body"]))) {
+ $tpl = get_markup_template('diaspora_reshare.tpl');
+ $msg = replace_macros($tpl, array(
+ '$root_handle' => xmlify($ret['root_handle']),
+ '$root_guid' => $ret['root_guid'],
+ '$guid' => $item['mid'],
+ '$handle' => xmlify($myaddr),
+ '$public' => $public,
+ '$created' => $created,
+ '$provider' => (($item['app']) ? $item['app'] : t('$projectname'))
+ ));
+ } else {
+ $tpl = get_markup_template('diaspora_post.tpl');
+ $msg = replace_macros($tpl, array(
+ '$body' => xmlify($body),
+ '$guid' => $item['mid'],
+ '$poll' => $poll,
+ '$handle' => xmlify($myaddr),
+ '$public' => $public,
+ '$created' => $created,
+ '$provider' => (($item['app']) ? $item['app'] : t('$projectname'))
+ ));
+ }
+
+ logger('diaspora_send_status: '.$owner['channel_name'].' -> '.$contact['xchan_name'].' base message: ' . $msg, LOGGER_DATA);
+
+ $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['channel_prvkey'],$contact['xchan_pubkey'],$public_batch)));
+
+ $qi = diaspora_queue($owner,$contact,$slap,$public_batch,$item['mid']);
+
+// logger('diaspora_send_status: guid: '.$item['mid'].' result '.$return_code, LOGGER_DEBUG);
+
+ if(count($images)) {
+ diaspora_send_images($item,$owner,$contact,$images,$public_batch,$item['mid']);
+ }
+
+ return $qi;
+}
+
+function diaspora_is_reshare($body) {
+
+ $body = trim($body);
+
+ // Skip if it isn't a pure repeated messages
+ // Does it start with a share?
+ if(strpos($body, "[share") > 0)
+ return(false);
+
+ // Does it end with a share?
+ if(strlen($body) > (strrpos($body, "[/share]") + 8))
+ return(false);
+
+ $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$1",$body);
+ // Skip if there is no shared message in there
+ if ($body == $attributes)
+ return(false);
+
+ $profile = "";
+ preg_match("/profile='(.*?)'/ism", $attributes, $matches);
+ if ($matches[1] != "")
+ $profile = $matches[1];
+
+ preg_match('/profile="(.*?)"/ism', $attributes, $matches);
+ if ($matches[1] != "")
+ $profile = $matches[1];
+
+ $ret= array();
+
+ $ret["root_handle"] = preg_replace("=https?://(.*)/u/(.*)=ism", "$2@$1", $profile);
+ if (($ret["root_handle"] == $profile) OR ($ret["root_handle"] == ""))
+ return(false);
+
+ $link = "";
+ preg_match("/link='(.*?)'/ism", $attributes, $matches);
+ if ($matches[1] != "")
+ $link = $matches[1];
+
+ preg_match('/link="(.*?)"/ism', $attributes, $matches);
+ if ($matches[1] != "")
+ $link = $matches[1];
+
+ $ret["root_guid"] = preg_replace("=https?://(.*)/posts/(.*)=ism", "$2", $link);
+ if (($ret["root_guid"] == $link) OR ($ret["root_guid"] == ""))
+ return(false);
+
+ return($ret);
+}
+
+function diaspora_send_images($item,$owner,$contact,$images,$public_batch = false) {
+ $a = get_app();
+ if(! count($images))
+ return;
+ $mysite = substr($a->get_baseurl(),strpos($a->get_baseurl(),'://') + 3) . '/photo';
+
+ $tpl = get_markup_template('diaspora_photo.tpl');
+ foreach($images as $image) {
+ if(! stristr($image['path'],$mysite))
+ continue;
+ $resource = str_replace('.jpg','',$image['file']);
+ $resource = substr($resource,0,strpos($resource,'-'));
+
+ $r = q("select * from photo where `resource_id` = '%s' and `uid` = %d limit 1",
+ dbesc($resource),
+ intval($owner['uid'])
+ );
+ if(! $r)
+ continue;
+ $public = (($r[0]['allow_cid'] || $r[0]['allow_gid'] || $r[0]['deny_cid'] || $r[0]['deny_gid']) ? 'false' : 'true' );
+ $msg = replace_macros($tpl,array(
+ '$path' => xmlify($image['path']),
+ '$filename' => xmlify($image['file']),
+ '$msg_guid' => xmlify($image['guid']),
+ '$guid' => xmlify($r[0]['resource_id']),
+ '$handle' => xmlify($image['handle']),
+ '$public' => xmlify($public),
+ '$created_at' => xmlify(datetime_convert('UTC','UTC',$r[0]['created'],'Y-m-d H:i:s \U\T\C'))
+ ));
+
+
+ logger('diaspora_send_photo: base message: ' . $msg, LOGGER_DATA);
+ $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['channel_prvkey'],$contact['xchan_pubkey'],$public_batch)));
+
+ return(diaspora_queue($owner,$contact,$slap,$public_batch,$item['mid']));
+ }
+
+}
+
+function diaspora_send_followup($item,$owner,$contact,$public_batch = false) {
+
+ $a = get_app();
+ $myaddr = $owner['channel_address'] . '@' . get_app()->get_hostname();
+ $theiraddr = $contact['xchan_addr'];
+
+ // Diaspora doesn't support threaded comments, but some
+ // versions of Diaspora (i.e. Diaspora-pistos) support
+ // likes on comments
+ if(($item['verb'] === ACTIVITY_LIKE || $item['verb'] === ACTIVITY_DISLIKE) && $item['thr_parent']) {
+ $p = q("select mid, parent_mid from item where mid = '%s' limit 1",
+ dbesc($item['thr_parent'])
+ );
+ }
+ else {
+ // The first item in the `item` table with the parent id is the parent. However, MySQL doesn't always
+ // return the items ordered by `item`.`id`, in which case the wrong item is chosen as the parent.
+ // The only item with `parent` and `id` as the parent id is the parent item.
+ $p = q("select * from item where parent = %d and id = %d limit 1",
+ intval($item['parent']),
+ intval($item['parent'])
+ );
+ }
+ if($p)
+ $parent = $p[0];
+ else
+ return;
+
+
+ if(($item['verb'] === ACTIVITY_LIKE) && ($parent['mid'] === $parent['parent_mid'])) {
+ $tpl = get_markup_template('diaspora_like.tpl');
+ $like = true;
+ $target_type = 'Post';
+ $positive = 'true';
+
+ if(intval($item['item_deleted']))
+ logger('diaspora_send_followup: received deleted "like". Those should go to diaspora_send_retraction');
+ }
+ else {
+ $tpl = get_markup_template('diaspora_comment.tpl');
+ $like = false;
+ }
+
+ if($item['diaspora_meta'] && ! $like) {
+ $diaspora_meta = json_decode($item['diaspora_meta'],true);
+ if($diaspora_meta) {
+ if(array_key_exists('iv',$diaspora_meta)) {
+ $key = get_config('system','prvkey');
+ $meta = json_decode(crypto_unencapsulate($diaspora_meta,$key),true);
+ }
+ else
+ $meta = $diaspora_meta;
+ }
+ $signed_text = $meta['signed_text'];
+ $authorsig = $meta['signature'];
+ $signer = $meta['signer'];
+ $text = $meta['body'];
+ }
+ else {
+ $text = bb2diaspora_itembody($item);
+
+ // sign it
+
+ if($like)
+ $signed_text = $item['mid'] . ';' . $target_type . ';' . $parent['mid'] . ';' . $positive . ';' . $myaddr;
+ else
+ $signed_text = $item['mid'] . ';' . $parent['mid'] . ';' . $text . ';' . $myaddr;
+
+ $authorsig = base64_encode(rsa_sign($signed_text,$owner['channel_prvkey'],'sha256'));
+
+ }
+
+ $msg = replace_macros($tpl,array(
+ '$guid' => xmlify($item['mid']),
+ '$parent_guid' => xmlify($parent['mid']),
+ '$target_type' =>xmlify($target_type),
+ '$authorsig' => xmlify($authorsig),
+ '$body' => xmlify($text),
+ '$positive' => xmlify($positive),
+ '$handle' => xmlify($myaddr)
+ ));
+
+ logger('diaspora_followup: base message: ' . $msg, LOGGER_DATA);
+
+ $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['channel_prvkey'],$contact['xchan_pubkey'],$public_batch)));
+
+
+ return(diaspora_queue($owner,$contact,$slap,$public_batch,$item['mid']));
+}
+
+
+function diaspora_send_relay($item,$owner,$contact,$public_batch = false) {
+
+
+ $a = get_app();
+ $myaddr = $owner['channel_address'] . '@' . get_app()->get_hostname();
+
+ $text = bb2diaspora_itembody($item);
+
+ $body = $text;
+
+ // Diaspora doesn't support threaded comments, but some
+ // versions of Diaspora (i.e. Diaspora-pistos) support
+ // likes on comments
+
+ // That version is now dead so detect a "sublike" and
+ // just send it as an activity.
+
+ $sublike = false;
+
+ if($item['verb'] === ACTIVITY_LIKE) {
+ if(($item['thr_parent']) && ($item['thr_parent'] !== $item['parent_mid'])) {
+ $sublike = true;
+ }
+ }
+
+ // The first item in the `item` table with the parent id is the parent. However, MySQL doesn't always
+ // return the items ordered by `item`.`id`, in which case the wrong item is chosen as the parent.
+ // The only item with `parent` and `id` as the parent id is the parent item.
+
+ $p = q("select * from item where parent = %d and id = %d limit 1",
+ intval($item['parent']),
+ intval($item['parent'])
+ );
+
+
+ if($p)
+ $parent = $p[0];
+ else {
+ logger('diaspora_send_relay: no parent');
+ return;
+ }
+
+ $like = false;
+ $relay_retract = false;
+ $sql_sign_id = 'iid';
+
+ if( intval($item['item_deleted'])) {
+ $relay_retract = true;
+
+ $target_type = ( ($item['verb'] === ACTIVITY_LIKE && (! $sublike)) ? 'Like' : 'Comment');
+
+ $sql_sign_id = 'retract_iid';
+ $tpl = get_markup_template('diaspora_relayable_retraction.tpl');
+ }
+ elseif(($item['verb'] === ACTIVITY_LIKE) && (! $sublike)) {
+ $like = true;
+
+ $target_type = ( $parent['mid'] === $parent['parent_mid'] ? 'Post' : 'Comment');
+// $positive = (intval($item['item_deleted']) ? 'false' : 'true');
+ $positive = 'true';
+
+ $tpl = get_markup_template('diaspora_like_relay.tpl');
+ }
+ else { // item is a comment
+ $tpl = get_markup_template('diaspora_comment_relay.tpl');
+ }
+
+ $diaspora_meta = (($item['diaspora_meta']) ? json_decode($item['diaspora_meta'],true) : '');
+ if($diaspora_meta) {
+ if(array_key_exists('iv',$diaspora_meta)) {
+ $key = get_config('system','prvkey');
+ $meta = json_decode(crypto_unencapsulate($diaspora_meta,$key),true);
+ }
+ else
+ $meta = $diaspora_meta;
+ $sender_signed_text = $meta['signed_text'];
+ $authorsig = $meta['signature'];
+ $handle = $meta['signer'];
+ $text = $meta['body'];
+ }
+ else
+ logger('diaspora_send_relay: original author signature not found');
+
+ /* Since the author signature is only checked by the parent, not by the relay recipients,
+ * I think it may not be necessary for us to do so much work to preserve all the original
+ * signatures. The important thing that Diaspora DOES need is the original creator's handle.
+ * Let's just generate that and forget about all the original author signature stuff.
+ *
+ * Note: this might be more of an problem if we want to support likes on comments for older
+ * versions of Diaspora (diaspora-pistos), but since there are a number of problems with
+ * doing that, let's ignore it for now.
+ *
+ *
+ */
+// bug - nomadic identity may/will affect diaspora_handle_from_contact
+ if(! $handle) {
+ if($item['author_xchan'] === $owner['channel_hash'])
+ $handle = $owner['channel_address'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3);
+ else
+ $handle = diaspora_handle_from_contact($item['author_xchan']);
+ }
+ if(! $handle) {
+ logger('diaspora_send_relay: no handle');
+ return;
+ }
+
+ if(! $sender_signed_text) {
+ if($relay_retract)
+ $sender_signed_text = $item['mid'] . ';' . $target_type;
+ elseif($like)
+ $sender_signed_text = $positive . ';' . $item['mid'] . ';' . $target_type . ';' . $parent['mid'] . ';' . $handle;
+ else
+ $sender_signed_text = $item['mid'] . ';' . $parent['mid'] . ';' . $text . ';' . $handle;
+ }
+
+ // Sign the relayable with the top-level owner's signature
+ //
+ // We'll use the $sender_signed_text that we just created, instead of the $signed_text
+ // stored in the database, because that provides the best chance that Diaspora will
+ // be able to reconstruct the signed text the same way we did. This is particularly a
+ // concern for the comment, whose signed text includes the text of the comment. The
+ // smallest change in the text of the comment, including removing whitespace, will
+ // make the signature verification fail. Since we translate from BB code to Diaspora's
+ // markup at the top of this function, which is AFTER we placed the original $signed_text
+ // in the database, it's hazardous to trust the original $signed_text.
+
+ $parentauthorsig = base64_encode(rsa_sign($sender_signed_text,$owner['channel_prvkey'],'sha256'));
+
+ if(! $text)
+ logger('diaspora_send_relay: no text');
+
+ $msg = replace_macros($tpl,array(
+ '$guid' => xmlify($item['mid']),
+ '$parent_guid' => xmlify($parent['mid']),
+ '$target_type' =>xmlify($target_type),
+ '$authorsig' => xmlify($authorsig),
+ '$parentsig' => xmlify($parentauthorsig),
+ '$body' => xmlify($text),
+ '$positive' => xmlify($positive),
+ '$handle' => xmlify($handle)
+ ));
+
+ logger('diaspora_send_relay: base message: ' . $msg, LOGGER_DATA);
+
+ $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['channel_prvkey'],$contact['xchan_pubkey'],$public_batch)));
+
+ return(diaspora_queue($owner,$contact,$slap,$public_batch,$item['mid']));
+
+}
+
+
+
+function diaspora_send_retraction($item,$owner,$contact,$public_batch = false) {
+
+ $a = get_app();
+ $myaddr = $owner['channel_address'] . '@' . get_app()->get_hostname();
+
+ // Check whether the retraction is for a top-level post or whether it's a relayable
+ if( $item['mid'] !== $item['parent_mid'] ) {
+
+ $tpl = get_markup_template('diaspora_relay_retraction.tpl');
+ $target_type = (($item['verb'] === ACTIVITY_LIKE) ? 'Like' : 'Comment');
+ }
+ else {
+
+ $tpl = get_markup_template('diaspora_signed_retract.tpl');
+ $target_type = 'StatusMessage';
+ }
+
+ $signed_text = $item['mid'] . ';' . $target_type;
+
+ $msg = replace_macros($tpl, array(
+ '$guid' => xmlify($item['mid']),
+ '$type' => xmlify($target_type),
+ '$handle' => xmlify($myaddr),
+ '$signature' => xmlify(base64_encode(rsa_sign($signed_text,$owner['channel_prvkey'],'sha256')))
+ ));
+
+ $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$owner,$contact,$owner['channel_prvkey'],$contact['xchan_pubkey'],$public_batch)));
+
+ return(diaspora_queue($owner,$contact,$slap,$public_batch,$item['mid']));
+}
+
+function diaspora_send_mail($item,$owner,$contact) {
+
+ $a = get_app();
+ $myaddr = $owner['channel_address'] . '@' . get_app()->get_hostname();
+
+ $r = q("select * from conv where guid = '%s' and uid = %d limit 1",
+ intval($item['conv_guid']),
+ intval($item['channel_id'])
+ );
+
+ if(! count($r)) {
+ logger('diaspora_send_mail: conversation not found.');
+ return;
+ }
+ $cnv = $r[0];
+ $cnv['subject'] = base64url_decode(str_rot47($cnv['subject']));
+
+ $conv = array(
+ 'guid' => xmlify($cnv['guid']),
+ 'subject' => xmlify($cnv['subject']),
+ 'created_at' => xmlify(datetime_convert('UTC','UTC',$cnv['created'],'Y-m-d H:i:s \U\T\C')),
+ 'diaspora_handle' => xmlify($cnv['creator']),
+ 'participant_handles' => xmlify($cnv['recips'])
+ );
+
+ if(array_key_exists('mail_obscured',$item) && intval($item['mail_obscured'])) {
+ if($item['title'])
+ $item['title'] = base64url_decode(str_rot47($item['title']));
+ if($item['body'])
+ $item['body'] = base64url_decode(str_rot47($item['body']));
+ }
+
+
+ $body = bb2diaspora($item['body']);
+ $created = datetime_convert('UTC','UTC',$item['created'],'Y-m-d H:i:s \U\T\C');
+
+ $signed_text = $item['mid'] . ';' . $cnv['guid'] . ';' . $body . ';'
+ . $created . ';' . $myaddr . ';' . $cnv['guid'];
+
+ $sig = base64_encode(rsa_sign($signed_text,$owner['channel_prvkey'],'sha256'));
+
+ $msg = array(
+ 'guid' => xmlify($item['mid']),
+ 'parent_guid' => xmlify($cnv['guid']),
+ 'parent_author_signature' => (($item['reply']) ? null : xmlify($sig)),
+ 'author_signature' => xmlify($sig),
+ 'text' => xmlify($body),
+ 'created_at' => xmlify($created),
+ 'diaspora_handle' => xmlify($myaddr),
+ 'conversation_guid' => xmlify($cnv['guid'])
+ );
+
+ if($item['reply']) {
+ $tpl = get_markup_template('diaspora_message.tpl');
+ $xmsg = replace_macros($tpl, array('$msg' => $msg));
+ }
+ else {
+ $conv['messages'] = array($msg);
+ $tpl = get_markup_template('diaspora_conversation.tpl');
+ $xmsg = replace_macros($tpl, array('$conv' => $conv));
+ }
+
+ logger('diaspora_conversation: ' . print_r($xmsg,true), LOGGER_DATA);
+
+ $slap = 'xml=' . urlencode(urlencode(diaspora_msg_build($xmsg,$owner,$contact,$owner['channel_prvkey'],$contact['xchan_pubkey'],false)));
+
+ return(diaspora_queue($owner,$contact,$slap,false,$item['mid']));
+
+
+}
+
+function diaspora_queue($owner,$contact,$slap,$public_batch,$message_id = '') {
+
+
+ $allowed = get_pconfig($owner['channel_id'],'system','diaspora_allowed');
+ if($allowed === false)
+ $allowed = 1;
+
+ if(! intval($allowed)) {
+ return false;
+ }
+
+ if($public_batch)
+ $dest_url = $contact['hubloc_callback'] . '/public';
+ else
+ $dest_url = $contact['hubloc_callback'] . '/users/' . $contact['hubloc_guid'];
+
+ logger('diaspora_queue: URL: ' . $dest_url, LOGGER_DEBUG);
+
+ if(intval(get_config('system','diaspora_test')))
+ return false;
+
+ $a = get_app();
+ $logid = random_string(4);
+
+ logger('diaspora_queue: ' . $logid . ' ' . $dest_url, LOGGER_DEBUG);
+
+ $hash = random_string();
+
+ q("insert into outq ( outq_hash, outq_account, outq_channel, outq_driver, outq_posturl, outq_async, outq_created, outq_updated, outq_notify, outq_msg ) values ( '%s', %d, %d, '%s', '%s', %d, '%s', '%s', '%s', '%s' )",
+ dbesc($hash),
+ intval($owner['account_id']),
+ intval($owner['channel_id']),
+ dbesc('post'),
+ dbesc($dest_url),
+ intval(1),
+ dbesc(datetime_convert()),
+ dbesc(datetime_convert()),
+ dbesc(''),
+ dbesc($slap)
+ );
+
+ if($message_id) {
+ q("insert into dreport ( dreport_mid, dreport_site, dreport_recip, dreport_result, dreport_time, dreport_xchan, dreport_queue ) values ( '%s','%s','%s','%s','%s','%s','%s' ) ",
+ dbesc($message_id),
+ dbesc($dest_url),
+ dbesc($dest_url),
+ dbesc('queued'),
+ dbesc(datetime_convert()),
+ dbesc($owner['channel_hash']),
+ dbesc($hash)
+ );
+ }
+
+ return $hash;
+
+}
+
+
+function diaspora_follow_allow(&$a, &$b) {
+
+ if($b['xchan']['xchan_network'] !== 'diaspora' && $b['xchan']['xchan_network'] !== 'friendica-over-diaspora')
+ return;
+
+ $allowed = get_pconfig($b['channel_id'],'system','diaspora_allowed');
+ if($allowed === false)
+ $allowed = 1;
+ $b['allowed'] = $allowed;
+
+}
+
+
+function diaspora_discover(&$a,&$b) {
+
+ require_once('include/network.php');
+
+ $result = array();
+ $network = null;
+ $diaspora = false;
+
+ $diaspora_base = '';
+ $diaspora_guid = '';
+ $diaspora_key = '';
+ $dfrn = false;
+
+ $x = old_webfinger($webbie);
+ if($x) {
+ logger('old_webfinger: ' . print_r($x,true));
+ foreach($x as $link) {
+ if($link['@attributes']['rel'] === NAMESPACE_DFRN)
+ $dfrn = unamp($link['@attributes']['href']);
+ if($link['@attributes']['rel'] === 'salmon')
+ $notify = unamp($link['@attributes']['href']);
+ if($link['@attributes']['rel'] === NAMESPACE_FEED)
+ $poll = unamp($link['@attributes']['href']);
+ if($link['@attributes']['rel'] === 'http://microformats.org/profile/hcard')
+ $hcard = unamp($link['@attributes']['href']);
+ if($link['@attributes']['rel'] === 'http://webfinger.net/rel/profile-page')
+ $profile = unamp($link['@attributes']['href']);
+ if($link['@attributes']['rel'] === 'http://portablecontacts.net/spec/1.0')
+ $poco = unamp($link['@attributes']['href']);
+ if($link['@attributes']['rel'] === 'http://joindiaspora.com/seed_location') {
+ $diaspora_base = unamp($link['@attributes']['href']);
+ $diaspora = true;
+ }
+ if($link['@attributes']['rel'] === 'http://joindiaspora.com/guid') {
+ $diaspora_guid = unamp($link['@attributes']['href']);
+ $diaspora = true;
+ }
+ if($link['@attributes']['rel'] === 'diaspora-public-key') {
+ $diaspora_key = base64_decode(unamp($link['@attributes']['href']));
+ if(strstr($diaspora_key,'RSA '))
+ $pubkey = rsatopem($diaspora_key);
+ else
+ $pubkey = $diaspora_key;
+ $diaspora = true;
+ }
+ }
+
+ if($diaspora && $diaspora_base && $diaspora_guid) {
+ $guid = $diaspora_guid;
+ $diaspora_base = trim($diaspora_base,'/');
+
+ $notify = $diaspora_base . '/receive';
+
+ if(strpos($webbie,'@')) {
+ $addr = str_replace('acct:', '', $webbie);
+ $hostname = substr($webbie,strpos($webbie,'@')+1);
+ }
+ $network = 'diaspora';
+ // until we get a dfrn layer, we'll use diaspora protocols for Friendica,
+ // but give it a different network so we can go back and fix these when we get proper support.
+ // It really should be just 'friendica' but we also want to distinguish
+ // between Friendica sites that we can use D* protocols with and those we can't.
+ // Some Friendica sites will have Diaspora disabled.
+ if($dfrn)
+ $network = 'friendica-over-diaspora';
+ if($hcard) {
+ $vcard = scrape_vcard($hcard);
+ $vcard['nick'] = substr($webbie,0,strpos($webbie,'@'));
+ if(! $vcard['fn'])
+ $vcard['fn'] = $webbie;
+ }
+
+ $r = q("select * from xchan where xchan_hash = '%s' limit 1",
+ dbesc($addr)
+ );
+
+ /**
+ *
+ * Diaspora communications are notoriously unreliable and receiving profile update messages (indeed any messages)
+ * are pretty much random luck. We'll check the timestamp of the xchan_name_date at a higher level and refresh
+ * this record once a month; because if you miss a profile update message and they update their profile photo or name
+ * you're otherwise stuck with stale info until they change their profile again - which could be years from now.
+ *
+ */
+
+ if($r) {
+ $r = q("update xchan set xchan_name = '%s', xchan_network = '%s', xchan_name_date = '%s' where xchan_hash = '%s' limit 1",
+ dbesc($vcard['fn']),
+ dbesc($network),
+ dbesc(datetime_convert()),
+ dbesc($addr)
+ );
+ }
+ else {
+
+ $r = q("insert into xchan ( xchan_hash, xchan_guid, xchan_pubkey, xchan_addr, xchan_url, xchan_name, xchan_network, xchan_name_date ) values ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s') ",
+ dbesc($addr),
+ dbesc($guid),
+ dbesc($pubkey),
+ dbesc($addr),
+ dbesc($profile),
+ dbesc($vcard['fn']),
+ dbesc($network),
+ dbescdate(datetime_convert())
+ );
+ }
+
+ $r = q("select * from hubloc where hubloc_hash = '%s' limit 1",
+ dbesc($webbie)
+ );
+
+ if(! $r) {
+
+ $r = q("insert into hubloc ( hubloc_guid, hubloc_hash, hubloc_addr, hubloc_network, hubloc_url, hubloc_host, hubloc_callback, hubloc_updated, hubloc_primary ) values ('%s','%s','%s','%s','%s','%s','%s','%s', 1)",
+ dbesc($guid),
+ dbesc($addr),
+ dbesc($addr),
+ dbesc($network),
+ dbesc(trim($diaspora_base,'/')),
+ dbesc($hostname),
+ dbesc($notify),
+ dbescdate(datetime_convert())
+ );
+ }
+ $photos = import_xchan_photo($vcard['photo'],$addr);
+ $r = q("update xchan set xchan_photo_date = '%s', xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s', xchan_photo_mimetype = '%s' where xchan_hash = '%s'",
+ dbescdate(datetime_convert('UTC','UTC',$arr['photo_updated'])),
+ dbesc($photos[0]),
+ dbesc($photos[1]),
+ dbesc($photos[2]),
+ dbesc($photos[3]),
+ dbesc($addr)
+ );
+ return true;
+
+ }
+
+ return false;
+
+/*
+ $vcard['fn'] = notags($vcard['fn']);
+ $vcard['nick'] = str_replace(' ','',notags($vcard['nick']));
+
+ $result['name'] = $vcard['fn'];
+ $result['nick'] = $vcard['nick'];
+ $result['guid'] = $guid;
+ $result['url'] = $profile;
+ $result['hostname'] = $hostname;
+ $result['addr'] = $addr;
+ $result['batch'] = $batch;
+ $result['notify'] = $notify;
+ $result['poll'] = $poll;
+ $result['request'] = $request;
+ $result['confirm'] = $confirm;
+ $result['poco'] = $poco;
+ $result['photo'] = $vcard['photo'];
+ $result['priority'] = $priority;
+ $result['network'] = $network;
+ $result['alias'] = $alias;
+ $result['pubkey'] = $pubkey;
+
+ logger('probe_url: ' . print_r($result,true), LOGGER_DEBUG);
+
+ return $result;
+
+*/
+
+/* Sample Diaspora result.
+
+Array
+(
+ [name] => Mike Macgirvin
+ [nick] => macgirvin
+ [guid] => a9174a618f8d269a
+ [url] => https://joindiaspora.com/u/macgirvin
+ [hostname] => joindiaspora.com
+ [addr] => macgirvin@joindiaspora.com
+ [batch] =>
+ [notify] => https://joindiaspora.com/receive
+ [poll] => https://joindiaspora.com/public/macgirvin.atom
+ [request] =>
+ [confirm] =>
+ [poco] =>
+ [photo] => https://joindiaspora.s3.amazonaws.com/uploads/images/thumb_large_fec4e6eef13ae5e56207.jpg
+ [priority] =>
+ [network] => diaspora
+ [alias] =>
+ [pubkey] => -----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtihtyIuRDWkDpCA+I1UaQ
+jI4S7k625+A7EEJm+pL2ZVSJxeCKiFeEgHBQENjLMNNm8l8F6blxgQqE6ZJ9Spa7f
+tlaXYTRCrfxKzh02L3hR7sNA+JS/nXJaUAIo+IwpIEspmcIRbD9GB7Wv/rr+M28uH
+31EeYyDz8QL6InU/bJmnCdFvmEMBQxJOw1ih9tQp7UNJAbUMCje0WYFzBz7sfcaHL
+OyYcCOqOCBLdGucUoJzTQ9iDBVzB8j1r1JkIHoEb2moUoKUp+tkCylNfd/3IVELF9
+7w1Qjmit3m50OrJk2DQOXvCW9KQxaQNdpRPSwhvemIt98zXSeyZ1q/YjjOwG0DWDq
+AF8aLj3/oQaZndTPy/6tMiZogKaijoxj8xFLuPYDTw5VpKquriVC0z8oxyRbv4t9v
+8JZZ9BXqzmayvY3xZGGp8NulrfjW+me2bKh0/df1aHaBwpZdDTXQ6kqAiS2FfsuPN
+vg57fhfHbL1yJ4oDbNNNeI0kJTGchXqerr8C20khU/cQ2Xt31VyEZtnTB665Ceugv
+kp3t2qd8UpAVKl430S5Quqx2ymfUIdxdW08CEjnoRNEL3aOWOXfbf4gSVaXmPCR4i
+LSIeXnd14lQYK/uxW/8cTFjcmddsKxeXysoQxbSa9VdDK+KkpZdgYXYrTTofXs6v+
+4afAEhRaaY+MCAwEAAQ==
+-----END PUBLIC KEY-----
+
+)
+*/
+
+
+
+
+ }
+}
+
+
+function diaspora_feature_settings_post(&$a,&$b) {
+
+ if($_POST['diaspora-submit']) {
+ set_pconfig(local_channel(),'system','diaspora_allowed',intval($_POST['dspr_allowed']));
+ set_pconfig(local_channel(),'system','diaspora_public_comments',intval($_POST['dspr_pubcomment']));
+ set_pconfig(local_channel(),'system','prevent_tag_hijacking',intval($_POST['dspr_hijack']));
+ info( t('Diaspora Protocol Settings updated.') . EOL);
+ }
+}
+
+
+function diaspora_feature_settings(&$a,&$s) {
+ $dspr_allowed = get_pconfig(local_channel(),'system','diaspora_allowed');
+ if($dspr_allowed === false)
+ $dspr_allowed = get_config('diaspora','allowed');
+ $pubcomments = get_pconfig(local_channel(),'system','diaspora_public_comments');
+ if($pubcomments === false)
+ $pubcomments = 1;
+ $hijacking = get_pconfig(local_channel(),'system','prevent_tag_hijacking');
+
+ $sc .= replace_macros(get_markup_template('field_checkbox.tpl'), array(
+ '$field' => array('dspr_allowed', t('Enable the (experimental) Diaspora protocol for this channel'), $dspr_allowed, '', $yes_no),
+ ));
+
+ $sc .= replace_macros(get_markup_template('field_checkbox.tpl'), array(
+ '$field' => array('dspr_pubcomment', t('Allow any Diaspora member to comment on your public posts'), $pubcomments, '', $yes_no),
+ ));
+
+ $sc .= replace_macros(get_markup_template('field_checkbox.tpl'), array(
+ '$field' => array('dspr_hijack', t('Prevent your hashtags from being redirected to other sites'), $hijacking, '', $yes_no),
+ ));
+
+
+ $s .= replace_macros(get_markup_template('generic_addon_settings.tpl'), array(
+ '$addon' => array('diaspora', ' ' . t('Diaspora Protocol Settings'), '', t('Submit')),
+ '$content' => $sc
+ ));
+
+ return;
+
+}
diff --git a/sources/addons/diaspora/p.php b/sources/addons/diaspora/p.php
new file mode 100644
index 00000000..3df8cd55
--- /dev/null
+++ b/sources/addons/diaspora/p.php
@@ -0,0 +1,52 @@
+get_hostname();
+
+ $item = $r[0];
+
+ $title = $item['title'];
+ $body = bb2diaspora_itembody($item);
+ $created = datetime_convert('UTC','UTC',$item['created'],'Y-m-d H:i:s \U\T\C');
+
+ $tpl = get_markup_template('diaspora_post.tpl');
+ $msg = replace_macros($tpl, array(
+ '$body' => xmlify($body),
+ '$guid' => $item['mid'],
+ '$handle' => xmlify($myaddr),
+ '$public' => 'true',
+ '$created' => $created,
+ '$provider' => (($item['app']) ? $item['app'] : t('$projectname'))
+ ));
+
+ header('Content-type: text/xml');
+ echo $msg;
+ killme();
+}
\ No newline at end of file
diff --git a/sources/addons/diaspora/receive.php b/sources/addons/diaspora/receive.php
new file mode 100644
index 00000000..fad4ae71
--- /dev/null
+++ b/sources/addons/diaspora/receive.php
@@ -0,0 +1,75 @@
+argv, true), LOGGER_DEBUG);
+
+ if((argc() == 2) && (argv(1) === 'public')) {
+ $public = true;
+ }
+ else {
+
+ if(argc() != 3 || argv(1) !== 'users')
+ http_status_exit(500);
+
+ $guid = argv(2);
+ $hn = str_replace('.','',$a->get_hostname());
+ if(($x = strpos($guid,$hn)) > 0)
+ $guid = substr($guid,0,$x);
+
+ // Diaspora sites *may* provide a truncated guid.
+
+ $r = q("SELECT * FROM channel left join xchan on channel_hash = xchan_hash WHERE channel_guid like '%s' AND channel_removed = 0 LIMIT 1",
+ dbesc($guid . '%')
+ );
+
+ if(! $r)
+ http_status_exit(500);
+
+ $importer = $r[0];
+ }
+
+ // It is an application/x-www-form-urlencoded that has been urlencoded twice.
+
+ logger('mod-diaspora: receiving post', LOGGER_DEBUG);
+
+ $xml = urldecode($_POST['xml']);
+
+ logger('mod-diaspora: new salmon ' . $xml, LOGGER_DATA);
+
+ if(! $xml)
+ http_status_exit(500);
+
+ logger('mod-diaspora: message is okay', LOGGER_DEBUG);
+
+ $msg = diaspora_decode($importer,$xml);
+
+ logger('mod-diaspora: decoded', LOGGER_DEBUG);
+
+ logger('mod-diaspora: decoded msg: ' . print_r($msg,true), LOGGER_DATA);
+
+ if(! is_array($msg))
+ http_status_exit(500);
+
+ logger('mod-diaspora: dispatching', LOGGER_DEBUG);
+
+ $ret = 0;
+ if($public)
+ diaspora_dispatch_public($msg);
+ else
+ $ret = diaspora_dispatch($importer,$msg);
+
+ http_status_exit(($ret) ? $ret : 200);
+ // NOTREACHED
+}
+
diff --git a/sources/addons/diaspost/diasphp.php b/sources/addons/diaspost/diasphp.php
new file mode 100644
index 00000000..d4c31b47
--- /dev/null
+++ b/sources/addons/diaspost/diasphp.php
@@ -0,0 +1,107 @@
+token_regex = '/content="(.*?)" name="csrf-token/';
+
+ $this->pod = $pod;
+ $this->cookiejar = tempnam(sys_get_temp_dir(), 'cookies');
+ }
+
+ function _fetch_token() {
+ $ch = curl_init();
+
+ curl_setopt ($ch, CURLOPT_URL, $this->pod . "/stream");
+ curl_setopt ($ch, CURLOPT_COOKIEFILE, $this->cookiejar);
+ curl_setopt ($ch, CURLOPT_COOKIEJAR, $this->cookiejar);
+ curl_setopt ($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt ($ch, CURLOPT_TIMEOUT, 60);
+
+
+ $output = curl_exec ($ch);
+ curl_close($ch);
+
+ // Token holen und zurückgeben
+ preg_match($this->token_regex, $output, $matches);
+ return $matches[1];
+ }
+
+ function login($username, $password) {
+ $datatopost = array(
+ 'user[username]' => $username,
+ 'user[password]' => $password,
+ 'authenticity_token' => $this->_fetch_token()
+ );
+
+ $poststr = http_build_query($datatopost);
+
+ // Adresse per cURL abrufen
+ $ch = curl_init();
+
+ curl_setopt ($ch, CURLOPT_URL, $this->pod . "/users/sign_in");
+ curl_setopt ($ch, CURLOPT_COOKIEFILE, $this->cookiejar);
+ curl_setopt ($ch, CURLOPT_COOKIEJAR, $this->cookiejar);
+ curl_setopt ($ch, CURLOPT_FOLLOWLOCATION, false);
+ curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt ($ch, CURLOPT_POST, true);
+ curl_setopt ($ch, CURLOPT_POSTFIELDS, $poststr);
+ curl_setopt ($ch, CURLOPT_TIMEOUT, 60);
+
+ curl_exec ($ch);
+ $info = curl_getinfo($ch);
+ curl_close($ch);
+
+ if($info['http_code'] != 302) {
+ throw new Exception('Login error '.print_r($info, true));
+ }
+
+ // Das Objekt zurückgeben, damit man Aurufe verketten kann.
+ return $this;
+ }
+
+ function post($text, $provider = "diasphp") {
+ // post-daten vorbereiten
+ $datatopost = json_encode(array(
+ 'aspect_ids' => 'public',
+ 'status_message' => array('text' => $text,
+ 'provider_display_name' => $provider)
+ ));
+
+ // header vorbereiten
+ $headers = array(
+ 'Content-Type: application/json',
+ 'accept: application/json',
+ 'x-csrf-token: '.$this->_fetch_token()
+ );
+
+ // Adresse per cURL abrufen
+ $ch = curl_init();
+
+ curl_setopt ($ch, CURLOPT_URL, $this->pod . "/status_messages");
+ curl_setopt ($ch, CURLOPT_COOKIEFILE, $this->cookiejar);
+ curl_setopt ($ch, CURLOPT_COOKIEJAR, $this->cookiejar);
+ curl_setopt ($ch, CURLOPT_FOLLOWLOCATION, false);
+ curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt ($ch, CURLOPT_POST, true);
+ curl_setopt ($ch, CURLOPT_POSTFIELDS, $datatopost);
+ curl_setopt ($ch, CURLOPT_HTTPHEADER, $headers);
+ curl_setopt ($ch, CURLOPT_TIMEOUT, 60);
+
+ curl_exec ($ch);
+ $info = curl_getinfo($ch);
+ curl_close($ch);
+
+ if($info['http_code'] != 201) {
+ throw new Exception('Post error '.print_r($info, true));
+ }
+
+ // Ende der möglichen Kette, gib mal "true" zurück.
+ return true;
+ }
+}
+?>
diff --git a/sources/addons/diaspost/diaspora.png b/sources/addons/diaspost/diaspora.png
new file mode 100644
index 0000000000000000000000000000000000000000..9666591638cc17fc8bb5a8a7eeed4b7cc83fb9a7
GIT binary patch
literal 952
zcmV;p14sOcP)kdg00009a7bBm000fw
z000fw0YWI7cmMzZ^JzmvP*7-ZbZ>KLZ*U+zZK3<12uwt~|df*uJ3vUQf-iKjLrpU5Em_
z07>B+2Ydjl2MGdd0rZyZAfN>3t*4U#s3)K+{!su1b!g-2=2rB1nx@(m_0sF>wh>=8
z`m%C$40W{P3*dVL;3$i>)tN0ztCO>Ye>cnL`YXLpQGhzx?7AIy7w>=m>|DSnOB*;a
z&P~yh0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F}0006%
zNkl93&HzuCC4jA^gJ;-=G0auAFC}jwX%uJ2G;Kt3iI>xpSDYO$h?(S7
zJPIUtlG(dhF2^GDQR4}jB#i~wMA+(C`$z*L8n|QF=ZLKCG_1a31waUZ!!#R`vSqel
zSlvYX;>CS3W}5#5HuKatCuk+g|5yR1Aw&WD1p2~FCi99ibTcGgBTaI0`lrP^BDF0a~(fn*x7wjxYch1nc|9
zOOR5`Zg~({#!FXj{Zz1v-wHX!O0Kv#-&wDV6Z16Gm}@VajHlEY>b+OAhmpX~`ZUK)
zx4e-zf#hlb?pZ7467x&QZ&8*1W38i$DWi0b`yBQE$W0dGAoWBkA&kWwGfXhZUG
+ */
+
+function diaspost_load() {
+ register_hook('post_local', 'addon/diaspost/diaspost.php', 'diaspost_post_local');
+ register_hook('notifier_normal', 'addon/diaspost/diaspost.php', 'diaspost_send');
+ register_hook('jot_networks', 'addon/diaspost/diaspost.php', 'diaspost_jot_nets');
+ register_hook('feature_settings', 'addon/diaspost/diaspost.php', 'diaspost_settings');
+ register_hook('feature_settings_post', 'addon/diaspost/diaspost.php', 'diaspost_settings_post');
+
+}
+function diaspost_unload() {
+ unregister_hook('post_local', 'addon/diaspost/diaspost.php', 'diaspost_post_local');
+ unregister_hook('notifier_normal', 'addon/diaspost/diaspost.php', 'diaspost_send');
+ unregister_hook('jot_networks', 'addon/diaspost/diaspost.php', 'diaspost_jot_nets');
+ unregister_hook('feature_settings', 'addon/diaspost/diaspost.php', 'diaspost_settings');
+ unregister_hook('feature_settings_post', 'addon/diaspost/diaspost.php', 'diaspost_settings_post');
+
+}
+
+
+function diaspost_jot_nets(&$a,&$b) {
+ if((! local_channel()) || (! perm_is_allowed(local_channel(),'','view_stream')))
+ return;
+
+ $diaspost_post = get_pconfig(local_channel(),'diaspost','post');
+ if(intval($diaspost_post) == 1) {
+ $diaspost_defpost = get_pconfig(local_channel(),'diaspost','post_by_default');
+ $selected = ((intval($diaspost_defpost) == 1) ? ' checked="checked" ' : '');
+ $b .= '';
+ }
+}
+
+function diaspost_queue_hook(&$a,&$b) {
+ $hostname = $a->get_hostname();
+
+ $qi = q("SELECT * FROM `queue` WHERE `network` = '%s'",
+ dbesc(NETWORK_DIASPORA2)
+ );
+ if(! count($qi))
+ return;
+
+ require_once('include/queue_fn.php');
+
+ foreach($qi as $x) {
+ if($x['network'] !== NETWORK_DIASPORA2)
+ continue;
+
+ logger('diaspost_queue: run');
+
+ $r = q("SELECT `user`.* FROM `user` LEFT JOIN `contact` on `contact`.`uid` = `user`.`uid`
+ WHERE `contact`.`self` = 1 AND `contact`.`id` = %d LIMIT 1",
+ intval($x['cid'])
+ );
+ if(! count($r))
+ continue;
+
+ $userdata = $r[0];
+
+ $diaspost_username = get_pconfig($userdata['uid'],'diaspost','diaspost_username');
+ $diaspost_password = z_unobscure(get_pconfig($userdata['uid'],'diaspost','diaspost_password'));
+ $diaspost_url = get_pconfig($userdata['uid'],'diaspost','diaspost_url');
+
+ $success = false;
+
+ if($diaspost_url && $diaspost_username && $diaspost_password) {
+ require_once("addon/diaspost/diasphp.php");
+
+ logger('diaspost_queue: able to post for user '.$diaspost_username);
+
+ $z = unserialize($x['content']);
+
+ $post = $z['post'];
+
+ logger('diaspost_queue: post: '.$post, LOGGER_DATA);
+
+ try {
+ logger('diaspost_queue: prepare', LOGGER_DEBUG);
+ $conn = new Diasphp($diaspost_url);
+ logger('diaspost_queue: try to log in '.$diaspost_username, LOGGER_DEBUG);
+ $conn->login($diaspost_username, $diaspost_password);
+ logger('diaspost_queue: try to send '.$body, LOGGER_DEBUG);
+ $conn->post($post, $hostname);
+
+ logger('diaspost_queue: send '.$userdata['uid'].' success', LOGGER_DEBUG);
+
+ $success = true;
+
+ remove_queue_item($x['id']);
+ } catch (Exception $e) {
+ logger("diaspost_queue: Send ".$userdata['uid']." failed: ".$e->getMessage(), LOGGER_DEBUG);
+ }
+ } else
+ logger('diaspost_queue: send '.$userdata['uid'].' missing username or password', LOGGER_DEBUG);
+
+ if (!$success) {
+ logger('diaspost_queue: delayed');
+ update_queue_time($x['id']);
+ }
+ }
+}
+
+function diaspost_settings(&$a,&$s) {
+
+ if(! local_channel())
+ return;
+
+ /* Add our stylesheet to the page so we can make our settings look nice */
+
+ //$a->page['htmlhead'] .= ' ' . "\r\n";
+
+ /* Get the current state of our config variables */
+
+ $enabled = get_pconfig(local_channel(),'diaspost','post');
+ $checked = (($enabled) ? '1' : false);
+ $css = (($enabled) ? '' : '-disabled');
+
+ $def_enabled = get_pconfig(local_channel(),'diaspost','post_by_default');
+
+ $def_checked = (($def_enabled) ? 1 : false);
+
+ $diaspost_username = get_pconfig(local_channel(), 'diaspost', 'diaspost_username');
+ $diaspost_password = z_unobscure(get_pconfig(local_channel(), 'diaspost', 'diaspost_password'));
+ $diaspost_url = get_pconfig(local_channel(), 'diaspost', 'diaspost_url');
+
+ $status = "";
+
+ if ($diaspost_username AND $diaspost_password AND $diaspost_url) {
+ try {
+ require_once("addon/diaspost/diasphp.php");
+
+ $conn = new Diasphp($diaspost_url);
+ $conn->login($diaspost_username, $diaspost_password);
+ } catch (Exception $e) {
+ $status = t("Can't login to your Diaspora account. Please check username and password and ensure you used the complete address (including http...)");
+ }
+ }
+
+ /* Add some HTML to the existing form */
+ if ($status) {
+ $sc .= '';
+ $sc .= '' . $status . ' ';
+ $sc .= '
';
+ }
+
+ $sc .= replace_macros(get_markup_template('field_checkbox.tpl'), array(
+ '$field' => array('diaspost', t('Enable Diaspost Post Plugin'), $checked, '', array(t('No'),t('Yes'))),
+ ));
+
+ $sc .= replace_macros(get_markup_template('field_input.tpl'), array(
+ '$field' => array('diaspost_username', t('Diaspora username'), $diaspost_username, '')
+ ));
+
+ $sc .= replace_macros(get_markup_template('field_password.tpl'), array(
+ '$field' => array('diaspost_password', t('Diaspora password'), $diaspost_password, '')
+ ));
+
+ $sc .= replace_macros(get_markup_template('field_input.tpl'), array(
+ '$field' => array('diaspost_url', t('Diaspora site URL'), $diaspost_url, 'Example: https://joindiaspora.com')
+ ));
+
+ $sc .= replace_macros(get_markup_template('field_checkbox.tpl'), array(
+ '$field' => array('diaspost_bydefault', t('Post to Diaspora by default'), $def_checked, '', array(t('No'),t('Yes'))),
+ ));
+
+ $s .= replace_macros(get_markup_template('generic_addon_settings.tpl'), array(
+ '$addon' => array('diaspost', ' ' . t('Diaspost Post Settings'), '', t('Submit')),
+ '$content' => $sc
+ ));
+
+ return;
+}
+
+
+function diaspost_settings_post(&$a,&$b) {
+
+ if(x($_POST,'diaspost-submit')) {
+
+ set_pconfig(local_channel(),'diaspost','post',intval($_POST['diaspost']));
+ set_pconfig(local_channel(),'diaspost','post_by_default',intval($_POST['diaspost_bydefault']));
+ set_pconfig(local_channel(),'diaspost','diaspost_username',trim($_POST['diaspost_username']));
+ set_pconfig(local_channel(),'diaspost','diaspost_password',z_obscure(trim($_POST['diaspost_password'])));
+ set_pconfig(local_channel(),'diaspost','diaspost_url',trim($_POST['diaspost_url']));
+
+ }
+
+}
+
+function diaspost_post_local(&$a,&$b) {
+
+ if($b['created'] != $b['edited'])
+ return;
+
+ if(! perm_is_allowed($b['uid'],'','view_stream'))
+ return;
+
+
+ if((! local_channel()) || (local_channel() != $b['uid']))
+ return;
+
+ if($b['item_private'])
+ return;
+
+ $diaspost_post = intval(get_pconfig(local_channel(),'diaspost','post'));
+
+ $diaspost_enable = (($diaspost_post && x($_REQUEST,'diaspost_enable')) ? intval($_REQUEST['diaspost_enable']) : 0);
+
+ if($_REQUEST['api_source'] && intval(get_pconfig(local_channel(),'diaspost','post_by_default')))
+ $diaspost_enable = 1;
+
+ if(! $diaspost_enable)
+ return;
+
+ if(strlen($b['postopts']))
+ $b['postopts'] .= ',';
+ $b['postopts'] .= 'diaspost';
+}
+
+
+
+
+function diaspost_send(&$a,&$b) {
+ $hostname = 'hubzilla ' . '(' . $a->get_hostname() . ')';
+
+ logger('diaspost_send: invoked',LOGGER_DEBUG);
+
+ if($b['mid'] != $b['parent_mid'])
+ return;
+
+ if((! is_item_normal($b)) || $b['item_private'] || ($b['created'] !== $b['edited']))
+ return;
+
+
+ if(! perm_is_allowed($b['uid'],'','view_stream'))
+ return;
+
+
+ if(! strstr($b['postopts'],'diaspost'))
+ return;
+
+
+ logger('diaspost_send: prepare posting', LOGGER_DEBUG);
+
+ $diaspost_username = get_pconfig($b['uid'],'diaspost','diaspost_username');
+ $diaspost_password = z_unobscure(get_pconfig($b['uid'],'diaspost','diaspost_password'));
+ $diaspost_url = get_pconfig($b['uid'],'diaspost','diaspost_url');
+
+ if($diaspost_url && $diaspost_username && $diaspost_password) {
+
+ logger('diaspost_send: all values seem to be okay', LOGGER_DEBUG);
+
+ require_once('include/bb2diaspora.php');
+ $tag_arr = array();
+ $tags = '';
+ $x = preg_match_all('/\#\[(.*?)\](.*?)\[/',$b['tag'],$matches,PREG_SET_ORDER);
+
+ if($x) {
+ foreach($matches as $mtch) {
+ $tag_arr[] = $mtch[2];
+ }
+ }
+ if(count($tag_arr))
+ $tags = implode(',',$tag_arr);
+
+ $title = $b['title'];
+ $body = $b['body'];
+ // Insert a newline before and after a quote
+ $body = str_ireplace("[quote", "\n\n[quote", $body);
+ $body = str_ireplace("[/quote]", "[/quote]\n\n", $body);
+
+ // strip bookmark indicators
+
+ $body = preg_replace('/\#\^\[([zu])rl/i', '[$1rl', $body);
+
+ $body = preg_replace('/\#\^http/i', 'http', $body);
+
+
+ if(intval(get_pconfig($item['uid'],'system','prevent_tag_hijacking'))) {
+ $new_tag = html_entity_decode('⋕',ENT_COMPAT,'UTF-8');
+ $new_mention = html_entity_decode('@',ENT_COMPAT,'UTF-8');
+
+ // #-tags
+ $body = preg_replace('/\#\[url/i', $new_tag . '[url', $body);
+ $body = preg_replace('/\#\[zrl/i', $new_tag . '[zrl', $body);
+ // @-mentions
+ $body = preg_replace('/\@\!?\[url/i', $new_mention . '[url', $body);
+ $body = preg_replace('/\@\!?\[zrl/i', $new_mention . '[zrl', $body);
+ }
+
+ // remove multiple newlines
+ do {
+ $oldbody = $body;
+ $body = str_replace("\n\n\n", "\n\n", $body);
+ } while ($oldbody != $body);
+
+ // convert to markdown
+ $body = bb2diaspora($body, false, true);
+
+ // Adding the title
+ if(strlen($title))
+ $body = "## ".html_entity_decode($title)."\n\n".$body;
+
+ require_once("addon/diaspost/diasphp.php");
+
+ try {
+ logger('diaspost_send: prepare', LOGGER_DEBUG);
+ $conn = new Diasphp($diaspost_url);
+ logger('diaspost_send: try to log in '.$diaspost_username, LOGGER_DEBUG);
+ $conn->login($diaspost_username, $diaspost_password);
+ logger('diaspost_send: try to send '.$body, LOGGER_DEBUG);
+
+ //throw new Exception('Test');
+ $conn->post($body, $hostname);
+
+ logger('diaspost_send: success');
+ } catch (Exception $e) {
+ logger("diaspost_send: Error submitting the post: " . $e->getMessage());
+
+// logger('diaspost_send: requeueing '.$b['uid'], LOGGER_DEBUG);
+
+// $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `self`", $b['uid']);
+// if (count($r))
+// $a->contact = $r[0]["id"];
+
+// $s = serialize(array('url' => $url, 'item' => $b['id'], 'post' => $body));
+// require_once('include/queue_fn.php');
+// add_to_queue($a->contact,NETWORK_DIASPORA2,$s);
+// notice(t('Diaspost post failed. Queued for retry.').EOL);
+ }
+ }
+}
diff --git a/sources/addons/dirstats/dirstats.php b/sources/addons/dirstats/dirstats.php
new file mode 100644
index 00000000..442f10c1
--- /dev/null
+++ b/sources/addons/dirstats/dirstats.php
@@ -0,0 +1,204 @@
+
+*/
+
+function dirstats_load() {
+ register_hook('cron_daily', 'addon/dirstats/dirstats.php', 'dirstats_cron');
+}
+function dirstats_unload() {
+ unregister_hook('cron_daily', 'addon/dirstats/dirstats.php', 'dirstats_cron');
+}
+function dirstats_module() {}
+
+
+function dirstats_init() {
+ if(! get_config('dirstats','hubcount'))
+ dirstats_cron($a,$b);
+
+}
+
+function dirstats_content(&$a) {
+
+ $hubcount = get_config('dirstats','hubcount');
+ $zotcount = get_config('dirstats','zotcount');
+ $friendicacount = get_config('dirstats','friendicacount');
+ $diasporacount = get_config('dirstats','diasporacount');
+ $channelcount = get_config('dirstats','channelcount');
+ $friendicachannelcount = get_config('dirstats','friendicachannelcount');
+ $diasporachannelcount = get_config('dirstats','diasporachannelcount');
+ $over35s = get_config('dirstats','over35s');
+ $under35s = get_config('dirstats','under35s');
+ $average = get_config('dirstats','averageage');
+ $chatrooms = get_config('dirstats','chatrooms');
+ $tags = get_config('dirstats','tags');
+
+ $ob = $a->get_observer();
+ $observer = $ob['xchan_hash'];
+ // Requested by Martin
+ $fountainofyouth = get_xconfig($observer, 'dirstats', 'averageage');
+ if (intval($fountainofyouth))
+ $average = $fountainofyouth;
+
+if (argv(1) == 'json') {
+ $dirstats = array (
+ 'hubcount' => $hubcount,
+ 'zotcount' => $zotcount,
+ 'friendicacount' => $friendicacount,
+ 'diasporacount' => $diasporacount,
+ 'channelcount' => $channelcount,
+ 'friendicachannelcount' => $friendicachannelcount,
+ 'diasporachannelcount' => $diasporachannelcount,
+ 'over35s' => $over35s,
+ 'under35s' => $under35s,
+ 'average' => $average,
+ 'chatrooms' => $chatrooms,
+ 'tags' => $tags
+ );
+ echo json_return_and_die($dirstats);
+ }
+
+ // Used by Hubzilla News
+ elseif (argv(1) == 'genpost' && get_config('dirstats','allowfiledump')) {
+ $result = '[b]Hub count[/b] : ' . $hubcount . "\xA" .
+ '[b]Hubzilla Hubs[/b] : ' . $zotcount . "\xA" .
+ '[b]Friendica Hubs[/b] : ' . $friendicacount . "\xA" .
+ '[b]Diaspora Pods[/b] : ' . $diasporacount . "\xA" .
+ '[b]Hubzilla Channels[/b] : ' . $channelcount . "\xA" .
+ '[b]Friendica Profiles[/b] : ' . $friendicachannelcount . "\xA" .
+ '[b]Diaspora Profiles[/b] : ' . $diasporachannelcount . "\xA" .
+ '[b]People aged 35 and above[/b] : ' . $over35s . "\xA" .
+ '[b]People aged 34 and below[/b] : ' . $under35s . "\xA" .
+ '[b]Average Age[/b] : ' . $average . "\xA" .
+ '[b]Known Chatrooms[/b] : ' . $chatrooms . "\xA" .
+ '[b]Unique Profile Tags[/b] : ' . $tags . "\xA";
+
+ file_put_contents('genpost', $result);
+ }
+else {
+ $tpl = get_markup_template( "dirstats.tpl", "addon/dirstats/" );
+ return replace_macros($tpl, array(
+ '$title' => t('Hubzilla Directory Stats'),
+ '$hubtitle' => t('Total Hubs'),
+ '$hubcount' => $hubcount,
+ '$zotlabel' => t('Hubzilla Hubs'),
+ '$zotcount' => $zotcount,
+ '$friendicalabel' => t('Friendica Hubs'),
+ '$friendicacount' => $friendicacount,
+ '$diasporalabel' => t('Diaspora Pods'),
+ '$diasporacount' => $diasporacount,
+ '$zotchanlabel' => t('Hubzilla Channels'),
+ '$channelcount' => $channelcount,
+ '$friendicachanlabel' => t('Friendica Channels'),
+ '$friendicachannelcount' => $friendicachannelcount,
+ '$diasporachanlabel' => t('Diaspora Channels'),
+ '$diasporachannelcount' => $diasporachannelcount,
+ '$over35label' => t('Aged 35 and above'),
+ '$over35s' => $over35s,
+ '$under35label' => t('Aged 34 and under'),
+ '$under35s' => $under35s,
+ '$averageagelabel' => t('Average Age'),
+ '$average' => $average,
+ '$chatlabel' => t('Known Chatrooms'),
+ '$chatrooms' => $chatrooms,
+ '$tagslabel' => t('Known Tags'),
+ '$tags' => $tags,
+ '$disclaimer' => t('Please note Diaspora and Friendica statistics are merely those **this directory** is aware of, and not all those known in the network. This also applies to chatrooms,')
+ ));
+ }
+}
+function dirstats_cron(&$a, $b) {
+ // Some hublocs are immortal and won't ever die - they all have null date for hubloc_connected and hubloc_updated
+ $r = q("SELECT count(distinct hubloc_host) as total FROM `hubloc` where not (hubloc_flags & %d) > 0 and not (hubloc_connected = %d and hubloc_updated = %d)",
+ intval(HUBLOC_FLAGS_DELETED),
+ dbesc(NULL_DATE),
+ dbesc(NULL_DATE)
+ );
+ if ($r) {
+ $hubcount = $r[0]['total'];
+ set_config('dirstats','hubcount',$hubcount);
+ }
+
+ $r = q("SELECT count(distinct hubloc_host) as total FROM `hubloc` where hubloc_network = 'zot' and not (hubloc_flags & %d) > 0 and not (hubloc_connected = %d and hubloc_updated = %d)",
+ intval(HUBLOC_FLAGS_DELETED),
+ dbesc(NULL_DATE),
+ dbesc(NULL_DATE)
+
+ );
+ if ($r) {
+ $zotcount = $r[0]['total'];
+ set_config('dirstats','zotcount',$zotcount);
+ }
+ $r = q("SELECT count(distinct hubloc_host) as total FROM `hubloc` where hubloc_network = 'friendica-over-diaspora'");
+ if ($r){
+ $friendicacount = $r[0]['total'];
+ set_config('dirstats','friendicacount',$friendicacount);
+ }
+ $r = q("SELECT count(distinct hubloc_host) as total FROM `hubloc` where hubloc_network = 'diaspora'");
+ if ($r) {
+ $diasporacount = $r[0]['total'];
+ set_config('dirstats','diasporacount',$diasporacount);
+ }
+ $r = q("SELECT count(distinct xchan_hash) as total FROM `xchan` where xchan_network = 'zot' and not (xchan_flags & %d) > 0",
+ intval(XCHAN_FLAGS_DELETED)
+ );
+ if ($r) {
+ $channelcount = $r[0]['total'];
+ set_config('dirstats','channelcount',$channelcount);
+ }
+ $r = q("SELECT count(distinct xchan_hash) as total FROM `xchan` where xchan_network = 'friendica-over-diaspora'");
+ if ($r) {
+ $friendicachannelcount = $r[0]['total'];
+ set_config('dirstats','friendicachannelcount',$friendicachannelcount);
+ }
+ $r = q("SELECT count(distinct xchan_hash) as total FROM `xchan` where xchan_network = 'diaspora'");
+ if ($r) {
+ $diasporachannelcount = $r[0]['total'];
+ set_config('dirstats','diasporachannelcount',$diasporachannelcount);
+ }
+ $r = q("select count(xprof_hash) as total from `xprof` where xprof_age >=35");
+ if ($r) {
+ $over35s = $r[0]['total'];
+ set_config('dirstats','over35s',$over35s);
+ }
+ $r = q("select count(xprof_hash) as total from `xprof` where xprof_age <=34 and xprof_age >=1");
+ if ($r) {
+ $under35s = $r[0]['total'];
+ set_config('dirstats','under35s',$under35s);
+ }
+
+ $r = q("select sum(xprof_age) as sum from xprof");
+ if ($r) {
+ $rr = q("select count(xprof_hash) as total from `xprof` where xprof_age >=1");
+ $total = $r[0]['sum'];
+ $number = $rr[0]['total'];
+ if($number)
+ $average = $total / $number;
+ else
+ $average = 0;
+ set_config('dirstats','averageage',$average);
+ }
+
+ $r = q("select count(distinct xchat_url) as total from `xchat`");
+ if ($r) {
+ $chatrooms = $r[0]['total'];
+ set_config('dirstats','chatrooms',$chatrooms);
+ }
+ $r = q("select count(distinct xtag_term) as total from xtag where xtag_flags = 0");
+ if ($r) {
+ $tags = $r[0]['total'];
+ set_config('dirstats','tags',$tags);
+ }
+}
diff --git a/sources/addons/dirstats/view/tpl/dirstats.tpl b/sources/addons/dirstats/view/tpl/dirstats.tpl
new file mode 100644
index 00000000..f1c3a132
--- /dev/null
+++ b/sources/addons/dirstats/view/tpl/dirstats.tpl
@@ -0,0 +1,20 @@
+
+
{{$title}}
+
{{$hubtitle}} : {{$hubcount}}
+
{{$zotlabel}} : {{$zotcount}}
+
{{$friendicalabel}} : {{$friendicacount}}
+
{{$diasporalabel}} : {{$diasporacount}}
+
+
{{$zotchanlabel}} : {{$channelcount}}
+
{{$friendicachanlabel}} : {{$friendicachannelcount}}
+
{{$diasporachanlabel}} : {{$diasporachannelcount}}
+
+
{{$over35label}} : {{$over35s}}
+
{{$under35label}} : {{$under35s}}
+
{{$averageagelabel}} : {{$average}}
+
+
{{$chatlabel}} : {{$chatrooms}}
+
{{$tagslabel}} : {{$tags}}
+
+
{{$disclaimer}}
+
diff --git a/sources/addons/donate/donate.apd b/sources/addons/donate/donate.apd
new file mode 100644
index 00000000..eff602d3
--- /dev/null
+++ b/sources/addons/donate/donate.apd
@@ -0,0 +1,3 @@
+url: $baseurl/donate
+name: Support Hubzilla
+photo: $baseurl/addon/donate/donate.png
diff --git a/sources/addons/donate/donate.php b/sources/addons/donate/donate.php
new file mode 100644
index 00000000..1eb6d57d
--- /dev/null
+++ b/sources/addons/donate/donate.php
@@ -0,0 +1,75 @@
+' . t('The Redmatrix/Hubzilla projects are provided primarily by volunteers giving their time and expertise - and often paying out of pocket for services they share with others.') . '
';
+$text .= '' . t('There is no corporate funding and no ads, and we do not collect and sell your personal information. (We don\'t control your personal information - you do .)') . '
';
+$text .= '' . t('Help support our ground-breaking work in decentralisation, web identity, and privacy.') . '
';
+
+$text .= '' . t('Your donations keep servers and services running and also helps us to provide innovative new features and continued development.') . '
';
+
+$o = replace_macros(get_markup_template('donate.tpl','addon/donate'),array(
+ '$header' => t('Donate'),
+ '$text' => $text,
+ '$choice' => t('Choose a project, developer, or public hub to support with a one-time donation'),
+ '$onetime' => t('Donate Now'),
+ '$repeat' => t('Or become a project sponsor (Hubzilla Project only)'),
+ '$note' => t('Please indicate if you would like your first name or full name (or nothing) to appear in our sponsor listing'),
+ '$subscribe' => t('Sponsor'),
+ '$contributors' => $contributors,
+ '$sponsors' => $sponsors,
+ '$thanks' => t('Special thanks to: '),
+));
+
+call_hooks('donate_plugin',$o);
+
+return $o;
+
+}
diff --git a/sources/addons/donate/donate.png b/sources/addons/donate/donate.png
new file mode 100644
index 0000000000000000000000000000000000000000..d932f0014756c2b094480ee637108d7138fcb63d
GIT binary patch
literal 7097
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
literal 0
HcmV?d00001
diff --git a/sources/addons/donate/tipping.jpg b/sources/addons/donate/tipping.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..f370ece78b8477128765f22b37031c9bb59884b3
GIT binary patch
literal 109373
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%0A+jDk|1a
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